add main menu and more hud elements
Former-commit-id: 29557c100e2b82597ced04df09f41c4f132ffc5e
85
voxygen/src/menu/main/mod.rs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
mod ui;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
use vek::*;
|
||||||
|
use common::clock::Clock;
|
||||||
|
use crate::{
|
||||||
|
PlayState,
|
||||||
|
PlayStateResult,
|
||||||
|
GlobalState,
|
||||||
|
window::{
|
||||||
|
Event,
|
||||||
|
Window,
|
||||||
|
},
|
||||||
|
session::SessionState,
|
||||||
|
};
|
||||||
|
use ui::MainMenuUi;
|
||||||
|
|
||||||
|
const FPS: u64 = 60;
|
||||||
|
|
||||||
|
pub struct MainMenuState {
|
||||||
|
main_menu_ui: MainMenuUi,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MainMenuState {
|
||||||
|
/// Create a new `MainMenuState`
|
||||||
|
pub fn new(window: &mut Window) -> Self {
|
||||||
|
Self {
|
||||||
|
main_menu_ui: MainMenuUi::new(window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// The background colour
|
||||||
|
const BG_COLOR: Rgba<f32> = Rgba { r: 0.0, g: 0.3, b: 1.0, a: 1.0 };
|
||||||
|
|
||||||
|
impl PlayState for MainMenuState {
|
||||||
|
fn play(&mut self, global_state: &mut GlobalState) -> PlayStateResult {
|
||||||
|
|
||||||
|
// Set up an fps clock
|
||||||
|
let mut clock = Clock::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Handle window events
|
||||||
|
for event in global_state.window.fetch_events() {
|
||||||
|
match event {
|
||||||
|
Event::Close => return PlayStateResult::Shutdown,
|
||||||
|
// Pass events to ui
|
||||||
|
Event::UiEvent(input) => {
|
||||||
|
self.main_menu_ui.handle_event(input);
|
||||||
|
}
|
||||||
|
// Ignore all other events
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global_state.window.renderer_mut().clear(BG_COLOR);
|
||||||
|
|
||||||
|
// Maintain the UI
|
||||||
|
self.main_menu_ui.maintain(global_state.window.renderer_mut());
|
||||||
|
// Check if there should be a login attempt
|
||||||
|
if let Some((username, address)) = self.main_menu_ui.login_attempt() {
|
||||||
|
// For now just start a new session
|
||||||
|
return PlayStateResult::Push(
|
||||||
|
Box::new(SessionState::new(&mut global_state.window.unwrap()), // TODO: Handle this error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the UI to the screen
|
||||||
|
self.main_menu_ui.render(global_state.window.renderer_mut());
|
||||||
|
|
||||||
|
// Finish the frame
|
||||||
|
global_state.window.renderer_mut().flush();
|
||||||
|
global_state.window
|
||||||
|
.swap_buffers()
|
||||||
|
.expect("Failed to swap window buffers");
|
||||||
|
|
||||||
|
// Wait for the next tick
|
||||||
|
clock.tick(Duration::from_millis(1000 / FPS));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str { "Title" }
|
||||||
|
}
|
290
voxygen/src/menu/main/ui.rs
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
use conrod_core::{
|
||||||
|
Positionable,
|
||||||
|
Sizeable,
|
||||||
|
Widget,
|
||||||
|
Labelable,
|
||||||
|
Colorable,
|
||||||
|
Borderable,
|
||||||
|
widget_ids,
|
||||||
|
event::Input,
|
||||||
|
image::Id as ImgId,
|
||||||
|
text::font::Id as FontId,
|
||||||
|
widget::{
|
||||||
|
Image,
|
||||||
|
Button,
|
||||||
|
Canvas,
|
||||||
|
TextBox,
|
||||||
|
text_box::Event as TextBoxEvent,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
window::Window,
|
||||||
|
render::Renderer,
|
||||||
|
ui::{Ui, ScaleMode}
|
||||||
|
};
|
||||||
|
|
||||||
|
widget_ids!{
|
||||||
|
struct Ids {
|
||||||
|
// Background and logo
|
||||||
|
bg,
|
||||||
|
v_logo,
|
||||||
|
// Login
|
||||||
|
login_button,
|
||||||
|
login_text,
|
||||||
|
address_text,
|
||||||
|
address_bg,
|
||||||
|
address_field,
|
||||||
|
username_text,
|
||||||
|
username_bg,
|
||||||
|
username_field,
|
||||||
|
// Buttons
|
||||||
|
servers_button,
|
||||||
|
servers_text,
|
||||||
|
settings_button,
|
||||||
|
settings_text,
|
||||||
|
quit_button,
|
||||||
|
quit_text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Imgs {
|
||||||
|
bg: ImgId,
|
||||||
|
v_logo: ImgId,
|
||||||
|
|
||||||
|
address_text: ImgId,
|
||||||
|
username_text: ImgId,
|
||||||
|
input_bg: ImgId,
|
||||||
|
|
||||||
|
login_text: ImgId,
|
||||||
|
login_button: ImgId,
|
||||||
|
login_button_hover: ImgId,
|
||||||
|
login_button_press: ImgId,
|
||||||
|
|
||||||
|
servers_text: ImgId,
|
||||||
|
settings_text: ImgId,
|
||||||
|
quit_text: ImgId,
|
||||||
|
button: ImgId,
|
||||||
|
button_hover: ImgId,
|
||||||
|
button_press: ImgId,
|
||||||
|
}
|
||||||
|
impl Imgs {
|
||||||
|
fn new(ui: &mut Ui, renderer: &mut Renderer) -> Imgs {
|
||||||
|
let mut load = |filename| {
|
||||||
|
let image = image::open(&[env!("CARGO_MANIFEST_DIR"), "/test_assets/ui/main/", filename].concat()).unwrap();
|
||||||
|
ui.new_image(renderer, &image).unwrap()
|
||||||
|
};
|
||||||
|
Imgs {
|
||||||
|
bg: load("bg.png"),
|
||||||
|
v_logo: load("v_logo_a01.png"),
|
||||||
|
|
||||||
|
// Input fields
|
||||||
|
address_text: load("text/server_address.png"),
|
||||||
|
username_text: load("text/username.png"),
|
||||||
|
input_bg: load("input_bg.png"),
|
||||||
|
|
||||||
|
// Login button
|
||||||
|
login_text: load("text/login.png"),
|
||||||
|
login_button: load("buttons/button_login.png"),
|
||||||
|
login_button_hover: load("buttons/button_login_hover.png"),
|
||||||
|
login_button_press: load("buttons/button_login_press.png"),
|
||||||
|
|
||||||
|
// Servers, settings, and quit buttons
|
||||||
|
servers_text: load("text/servers.png"),
|
||||||
|
settings_text: load("text/settings.png"),
|
||||||
|
quit_text: load("text/quit.png"),
|
||||||
|
button: load("buttons/button.png"),
|
||||||
|
button_hover: load("buttons/button_hover.png"),
|
||||||
|
button_press: load("buttons/button_press.png"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MainMenuUi {
|
||||||
|
ui: Ui,
|
||||||
|
ids: Ids,
|
||||||
|
imgs: Imgs,
|
||||||
|
font_id: FontId,
|
||||||
|
username: String,
|
||||||
|
server_address: String,
|
||||||
|
attempt_login: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MainMenuUi {
|
||||||
|
pub fn new(window: &mut Window) -> Self {
|
||||||
|
let mut ui = Ui::new(window).unwrap();
|
||||||
|
// TODO: adjust/remove this, right now it is used to demonstrate window scaling functionality
|
||||||
|
ui.scaling_mode(ScaleMode::RelativeToWindow([1920.0, 1080.0].into()));
|
||||||
|
// Generate ids
|
||||||
|
let ids = Ids::new(ui.id_generator());
|
||||||
|
// Load images
|
||||||
|
let imgs = Imgs::new(&mut ui, window.renderer_mut());
|
||||||
|
// Load font
|
||||||
|
let font_id = ui.new_font(conrod_core::text::font::from_file(
|
||||||
|
concat!(env!("CARGO_MANIFEST_DIR"), "/test_assets/font/Metamorphous-Regular.ttf")
|
||||||
|
).unwrap());
|
||||||
|
Self {
|
||||||
|
ui,
|
||||||
|
imgs,
|
||||||
|
ids,
|
||||||
|
font_id,
|
||||||
|
username: "Username".to_string(),
|
||||||
|
server_address: "Server-Address".to_string(),
|
||||||
|
attempt_login: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: probably a better way to do this
|
||||||
|
pub fn login_attempt(&mut self) -> Option<(String, String)> {
|
||||||
|
if self.attempt_login {
|
||||||
|
self.attempt_login = false;
|
||||||
|
Some((self.username.clone(), self.server_address.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_layout(&mut self) {
|
||||||
|
let ref mut ui_widgets = self.ui.set_widgets();
|
||||||
|
// Background image & Veloren logo
|
||||||
|
Image::new(self.imgs.bg)
|
||||||
|
.middle_of(ui_widgets.window)
|
||||||
|
.set(self.ids.bg, ui_widgets);
|
||||||
|
Image::new(self.imgs.v_logo)
|
||||||
|
.w_h(346.0, 111.0)
|
||||||
|
.top_left_with_margins(30.0, 40.0)
|
||||||
|
.set(self.ids.v_logo, ui_widgets);
|
||||||
|
|
||||||
|
// Input fields
|
||||||
|
// Used when the login button is pressed, or enter is pressed within input field
|
||||||
|
macro_rules! login {
|
||||||
|
() => {
|
||||||
|
self.attempt_login = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use conrod_core::color::TRANSPARENT;
|
||||||
|
// Username
|
||||||
|
// TODO: get a lower resolution and cleaner input_bg.png
|
||||||
|
Image::new(self.imgs.input_bg)
|
||||||
|
.w_h(672.0/2.0, 166.0/2.0)
|
||||||
|
.middle_of(ui_widgets.window)
|
||||||
|
.set(self.ids.username_bg, ui_widgets);
|
||||||
|
Image::new(self.imgs.username_text)
|
||||||
|
.w_h(149.0, 24.0)
|
||||||
|
.up(0.0)
|
||||||
|
.align_left()
|
||||||
|
.set(self.ids.username_text, ui_widgets);
|
||||||
|
// TODO: figure out why cursor is rendered inconsistently
|
||||||
|
for event in TextBox::new(&self.username)
|
||||||
|
.w_h(580.0/2.0, 60.0/2.0)
|
||||||
|
.mid_bottom_with_margin_on(self.ids.username_bg, 44.0/2.0)
|
||||||
|
.font_size(20)
|
||||||
|
// transparent background
|
||||||
|
.color(TRANSPARENT)
|
||||||
|
.border_color(TRANSPARENT)
|
||||||
|
.set(self.ids.username_field, ui_widgets)
|
||||||
|
{
|
||||||
|
match event {
|
||||||
|
TextBoxEvent::Update(username) => {
|
||||||
|
// Note: TextBox limits the input string length to what fits in it
|
||||||
|
self.username = username.to_string();
|
||||||
|
}
|
||||||
|
TextBoxEvent::Enter => login!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Server address
|
||||||
|
Image::new(self.imgs.address_text)
|
||||||
|
.w_h(227.0, 28.0)
|
||||||
|
.down_from(self.ids.username_bg, 10.0)
|
||||||
|
.align_left_of(self.ids.username_bg)
|
||||||
|
.set(self.ids.address_text, ui_widgets);
|
||||||
|
Image::new(self.imgs.input_bg)
|
||||||
|
.w_h(672.0/2.0, 166.0/2.0)
|
||||||
|
.down(0.0)
|
||||||
|
.align_left()
|
||||||
|
.set(self.ids.address_bg, ui_widgets);
|
||||||
|
for event in TextBox::new(&self.server_address)
|
||||||
|
.w_h(580.0/2.0, 60.0/2.0)
|
||||||
|
.mid_bottom_with_margin_on(self.ids.address_bg, 44.0/2.0)
|
||||||
|
.font_size(20)
|
||||||
|
// transparent background
|
||||||
|
.color(TRANSPARENT)
|
||||||
|
.border_color(TRANSPARENT)
|
||||||
|
.set(self.ids.address_field, ui_widgets)
|
||||||
|
{
|
||||||
|
match event {
|
||||||
|
TextBoxEvent::Update(server_address) => {
|
||||||
|
self.server_address = server_address.to_string();
|
||||||
|
}
|
||||||
|
TextBoxEvent::Enter => login!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Login button
|
||||||
|
if Button::image(self.imgs.login_button)
|
||||||
|
.hover_image(self.imgs.login_button_hover)
|
||||||
|
.press_image(self.imgs.login_button_press)
|
||||||
|
.w_h(258.0, 68.0)
|
||||||
|
.down_from(self.ids.address_bg, 20.0)
|
||||||
|
.align_middle_x_of(self.ids.address_bg)
|
||||||
|
.set(self.ids.login_button, ui_widgets)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
login!();
|
||||||
|
}
|
||||||
|
Image::new(self.imgs.login_text)
|
||||||
|
.w_h(83.0, 34.0)
|
||||||
|
.graphics_for(self.ids.login_button) // capture the input for the button
|
||||||
|
.middle_of(self.ids.login_button)
|
||||||
|
.set(self.ids.login_text, ui_widgets);
|
||||||
|
|
||||||
|
// Other buttons
|
||||||
|
// Quit
|
||||||
|
Button::image(self.imgs.button)
|
||||||
|
.w_h(203.0, 53.0)
|
||||||
|
.bottom_left_with_margins_on(ui_widgets.window, 60.0, 30.0)
|
||||||
|
.hover_image(self.imgs.button_hover)
|
||||||
|
.press_image(self.imgs.button_press)
|
||||||
|
.set(self.ids.quit_button, ui_widgets);
|
||||||
|
Image::new(self.imgs.quit_text)
|
||||||
|
.w_h(52.0, 26.0)
|
||||||
|
.graphics_for(self.ids.quit_button) // capture the input for the button
|
||||||
|
.middle_of(self.ids.quit_button)
|
||||||
|
.set(self.ids.quit_text, ui_widgets);
|
||||||
|
// Settings
|
||||||
|
Button::image(self.imgs.button)
|
||||||
|
.w_h(203.0, 53.0)
|
||||||
|
.up_from(self.ids.quit_button, 8.0)
|
||||||
|
.hover_image(self.imgs.button_hover)
|
||||||
|
.press_image(self.imgs.button_press)
|
||||||
|
.set(self.ids.settings_button, ui_widgets);
|
||||||
|
Image::new(self.imgs.settings_text)
|
||||||
|
.w_h(98.0, 28.0)
|
||||||
|
.graphics_for(self.ids.settings_button)
|
||||||
|
.middle_of(self.ids.settings_button)
|
||||||
|
.set(self.ids.settings_text, ui_widgets);
|
||||||
|
// Servers
|
||||||
|
Button::image(self.imgs.button)
|
||||||
|
.w_h(203.0, 53.0)
|
||||||
|
.up_from(self.ids.settings_button, 8.0)
|
||||||
|
.hover_image(self.imgs.button_hover)
|
||||||
|
.press_image(self.imgs.button_press)
|
||||||
|
.set(self.ids.servers_button, ui_widgets);
|
||||||
|
Image::new(self.imgs.servers_text)
|
||||||
|
.w_h(93.0, 20.0)
|
||||||
|
.graphics_for(self.ids.servers_button)
|
||||||
|
.middle_of(self.ids.servers_button)
|
||||||
|
.set(self.ids.servers_text, ui_widgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_event(&mut self, input: Input) {
|
||||||
|
self.ui.handle_event(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn maintain(&mut self, renderer: &mut Renderer) {
|
||||||
|
self.update_layout();
|
||||||
|
self.ui.maintain(renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, renderer: &mut Renderer) {
|
||||||
|
self.ui.render(renderer);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,3 @@
|
|||||||
pub mod title;
|
pub mod title;
|
||||||
mod title_ui;
|
pub mod main;
|
||||||
pub mod test_hud;
|
pub mod test_hud;
|
||||||
|
@ -34,11 +34,17 @@ widget_ids!{
|
|||||||
menu_bot,
|
menu_bot,
|
||||||
menu_canvas,
|
menu_canvas,
|
||||||
menu_buttons[],
|
menu_buttons[],
|
||||||
|
bag_belt,
|
||||||
|
belt_buttons[],
|
||||||
|
mmap_frame,
|
||||||
|
sbar_bg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make macro to mimic widget_ids! for images ids or find another solution to simplify addition of new images.
|
// TODO: make macro to mimic widget_ids! for images ids or find another solution to simplify addition of new images.
|
||||||
struct Imgs {
|
struct Imgs {
|
||||||
|
//Missing: ActionBar, Health/Mana/Energy Bar & Char Window BG/Frame
|
||||||
|
// Bag
|
||||||
bag: ImgId,
|
bag: ImgId,
|
||||||
bag_hover: ImgId,
|
bag_hover: ImgId,
|
||||||
bag_press: ImgId,
|
bag_press: ImgId,
|
||||||
@ -46,21 +52,34 @@ struct Imgs {
|
|||||||
bag_open_hover: ImgId,
|
bag_open_hover: ImgId,
|
||||||
bag_open_press: ImgId,
|
bag_open_press: ImgId,
|
||||||
bag_contents: ImgId,
|
bag_contents: ImgId,
|
||||||
|
// Close button
|
||||||
close_button: ImgId,
|
close_button: ImgId,
|
||||||
close_button_hover: ImgId,
|
close_button_hover: ImgId,
|
||||||
close_button_press: ImgId,
|
close_button_press: ImgId,
|
||||||
|
// Settings belt
|
||||||
|
belt_bg: ImgId,
|
||||||
|
belt_grid: ImgId,
|
||||||
|
belt_grid_hover: ImgId,
|
||||||
|
belt_grid_press: ImgId,
|
||||||
|
//belt_grid_open: ImgId,
|
||||||
|
// Menu
|
||||||
menu_top: ImgId,
|
menu_top: ImgId,
|
||||||
menu_mid: ImgId,
|
menu_mid: ImgId,
|
||||||
menu_bot: ImgId,
|
menu_bot: ImgId,
|
||||||
menu_button: ImgId,
|
menu_button: ImgId,
|
||||||
}
|
// MiniMap
|
||||||
|
mmap_frame: ImgId,
|
||||||
|
// SkillBar
|
||||||
|
sbar_bg: ImgId
|
||||||
|
}
|
||||||
impl Imgs {
|
impl Imgs {
|
||||||
fn new(ui: &mut Ui, renderer: &mut Renderer) -> Imgs {
|
fn new(ui: &mut Ui, renderer: &mut Renderer) -> Imgs {
|
||||||
let mut load = |filename| {
|
let mut load = |filename| {
|
||||||
let image = image::open(&[env!("CARGO_MANIFEST_DIR"), "/test_assets/hud/", filename].concat()).unwrap();
|
let image = image::open(&[env!("CARGO_MANIFEST_DIR"), "/test_assets/ui/hud/", filename].concat()).unwrap();
|
||||||
ui.new_image(renderer, &image).unwrap()
|
ui.new_image(renderer, &image).unwrap()
|
||||||
};
|
};
|
||||||
Imgs {
|
Imgs {
|
||||||
|
// Bag
|
||||||
bag: load("bag/icon/0_bag.png"),
|
bag: load("bag/icon/0_bag.png"),
|
||||||
bag_hover: load("bag/icon/1_bag_hover.png"),
|
bag_hover: load("bag/icon/1_bag_hover.png"),
|
||||||
bag_press: load("bag/icon/2_bag_press.png"),
|
bag_press: load("bag/icon/2_bag_press.png"),
|
||||||
@ -68,13 +87,25 @@ impl Imgs {
|
|||||||
bag_open_hover: load("bag/icon/4_bag_open_hover.png"),
|
bag_open_hover: load("bag/icon/4_bag_open_hover.png"),
|
||||||
bag_open_press: load("bag/icon/5_bag_open_press.png"),
|
bag_open_press: load("bag/icon/5_bag_open_press.png"),
|
||||||
bag_contents: load("bag/bg.png"),
|
bag_contents: load("bag/bg.png"),
|
||||||
|
// Close button
|
||||||
close_button: load("x/0_x.png"),
|
close_button: load("x/0_x.png"),
|
||||||
close_button_hover: load("x/1_x_hover.png"),
|
close_button_hover: load("x/1_x_hover.png"),
|
||||||
close_button_press: load("x/2_x_press.png"),
|
close_button_press: load("x/2_x_press.png"),
|
||||||
|
// Settings belt
|
||||||
|
belt_bg: load("belt/belt_bg.png"),
|
||||||
|
belt_grid: load("belt/belt_grid.png"),
|
||||||
|
belt_grid_hover: load("belt/belt_hover.png"),
|
||||||
|
belt_grid_press: load("belt/belt_press.png"),
|
||||||
|
//belt_grid_open: load("belt/belt_open.png"),
|
||||||
|
// Menu
|
||||||
menu_button: load("menu/main/menu_button.png"),
|
menu_button: load("menu/main/menu_button.png"),
|
||||||
menu_top: load("menu/main/menu_top.png"),
|
menu_top: load("menu/main/menu_top.png"),
|
||||||
menu_mid: load("menu/main/menu_mid.png"),
|
menu_mid: load("menu/main/menu_mid.png"),
|
||||||
menu_bot: load("menu/main/menu_bottom.png"),
|
menu_bot: load("menu/main/menu_bottom.png"),
|
||||||
|
// MiniMap
|
||||||
|
mmap_frame: load("mmap/mmap_frame.png"),
|
||||||
|
// SkillBar
|
||||||
|
sbar_bg: load("skill_bar/sbar_bg.png"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,6 +125,7 @@ impl TestHud {
|
|||||||
// Generate ids
|
// Generate ids
|
||||||
let mut ids = Ids::new(ui.id_generator());
|
let mut ids = Ids::new(ui.id_generator());
|
||||||
ids.menu_buttons.resize(5, &mut ui.id_generator());
|
ids.menu_buttons.resize(5, &mut ui.id_generator());
|
||||||
|
ids.belt_buttons.resize(6, &mut ui.id_generator());
|
||||||
// Load images
|
// Load images
|
||||||
let imgs = Imgs::new(&mut ui, window.renderer_mut());
|
let imgs = Imgs::new(&mut ui, window.renderer_mut());
|
||||||
// Load font
|
// Load font
|
||||||
@ -129,7 +161,7 @@ impl TestHud {
|
|||||||
.bottom_right_with_margins(88.0, 68.0)
|
.bottom_right_with_margins(88.0, 68.0)
|
||||||
.set(self.ids.bag_contents, ui_widgets);
|
.set(self.ids.bag_contents, ui_widgets);
|
||||||
|
|
||||||
// X-Button
|
// X-button
|
||||||
if Button::image(self.imgs.close_button)
|
if Button::image(self.imgs.close_button)
|
||||||
.w_h(144.0/4.0, 144.0/4.0)
|
.w_h(144.0/4.0, 144.0/4.0)
|
||||||
.hover_image(self.imgs.close_button_hover)
|
.hover_image(self.imgs.close_button_hover)
|
||||||
@ -141,7 +173,34 @@ impl TestHud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
//Bag
|
// Belt menu
|
||||||
|
Image::new(self.imgs.belt_bg)
|
||||||
|
.w_h(448.0/2.0, 56.0/2.0)
|
||||||
|
.bottom_left_with_margins_on(self.ids.bag, 5.0, -167.0)
|
||||||
|
.set(self.ids.bag_belt, ui_widgets);
|
||||||
|
// Belt buttons
|
||||||
|
for i in 0..6 {
|
||||||
|
Button::image(self.imgs.belt_grid)
|
||||||
|
.w_h(56.0/2.0, 56.0/2.0)
|
||||||
|
.bottom_left_with_margins_on(self.ids.bag_belt, 0.0, 28.0 * i as f64)
|
||||||
|
.hover_image(self.imgs.belt_grid_hover)
|
||||||
|
.press_image(self.imgs.belt_grid_press)
|
||||||
|
.set(self.ids.belt_buttons[i], ui_widgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimap frame
|
||||||
|
Image::new(self.imgs.mmap_frame)
|
||||||
|
.w_h(1232.0/8.0, 976.0/8.0)
|
||||||
|
.top_right_of(ui_widgets.window)
|
||||||
|
.set(self.ids.mmap_frame, ui_widgets);
|
||||||
|
|
||||||
|
// Action bar
|
||||||
|
Image::new(self.imgs.sbar_bg)
|
||||||
|
.w_h(2240.0/8.0, 906.0/8.0)
|
||||||
|
.mid_bottom_of(ui_widgets.window)
|
||||||
|
.set(self.ids.sbar_bg, ui_widgets);
|
||||||
|
|
||||||
|
// Bag
|
||||||
Button::image(if self.bag_open {self.imgs.bag_open} else {self.imgs.bag})
|
Button::image(if self.bag_open {self.imgs.bag_open} else {self.imgs.bag})
|
||||||
.bottom_right_with_margin_on(ui_widgets.window, 20.0)
|
.bottom_right_with_margin_on(ui_widgets.window, 20.0)
|
||||||
.hover_image(if self.bag_open {self.imgs.bag_open_hover} else {self.imgs.bag_hover})
|
.hover_image(if self.bag_open {self.imgs.bag_open_hover} else {self.imgs.bag_hover})
|
||||||
@ -149,8 +208,7 @@ impl TestHud {
|
|||||||
.w_h(420.0/4.0, 480.0/4.0)
|
.w_h(420.0/4.0, 480.0/4.0)
|
||||||
.set(self.ids.bag, ui_widgets);
|
.set(self.ids.bag, ui_widgets);
|
||||||
|
|
||||||
|
// An attempt to make a resizable image based container for buttons
|
||||||
// Attempt to make resizable image based container for buttons
|
|
||||||
// Maybe this could be made into a Widget type if it is useful
|
// Maybe this could be made into a Widget type if it is useful
|
||||||
if self.menu_open {
|
if self.menu_open {
|
||||||
let num = self.ids.menu_buttons.len();
|
let num = self.ids.menu_buttons.len();
|
||||||
@ -163,9 +221,6 @@ impl TestHud {
|
|||||||
Image::new(self.imgs.menu_top)
|
Image::new(self.imgs.menu_top)
|
||||||
.w_h(106.0, 28.0)
|
.w_h(106.0, 28.0)
|
||||||
.mid_top_of(self.ids.menu_canvas)
|
.mid_top_of(self.ids.menu_canvas)
|
||||||
// Does not work because of bug in conrod, but above line is equivalent
|
|
||||||
//.parent(ids.menu_canvas)
|
|
||||||
//.mid_top()
|
|
||||||
.set(self.ids.menu_top, ui_widgets);
|
.set(self.ids.menu_top, ui_widgets);
|
||||||
// Bottom of Menu
|
// Bottom of Menu
|
||||||
// Note: conrod defaults to the last used parent
|
// Note: conrod defaults to the last used parent
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// Library
|
mod ui;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
use common::clock::Clock;
|
||||||
|
|
||||||
// Crate
|
|
||||||
use crate::{
|
use crate::{
|
||||||
PlayState,
|
PlayState,
|
||||||
PlayStateResult,
|
PlayStateResult,
|
||||||
@ -11,11 +11,11 @@ use crate::{
|
|||||||
Event,
|
Event,
|
||||||
Window,
|
Window,
|
||||||
},
|
},
|
||||||
session::SessionState,
|
|
||||||
};
|
};
|
||||||
|
use super::main::MainMenuState;
|
||||||
|
use ui::TitleUi;
|
||||||
|
|
||||||
// Local
|
const FPS: u64 = 60;
|
||||||
use super::title_ui::TitleUi;
|
|
||||||
|
|
||||||
pub struct TitleState {
|
pub struct TitleState {
|
||||||
title_ui: TitleUi,
|
title_ui: TitleUi,
|
||||||
@ -36,14 +36,18 @@ const BG_COLOR: Rgba<f32> = Rgba { r: 0.0, g: 0.3, b: 1.0, a: 1.0 };
|
|||||||
|
|
||||||
impl PlayState for TitleState {
|
impl PlayState for TitleState {
|
||||||
fn play(&mut self, global_state: &mut GlobalState) -> PlayStateResult {
|
fn play(&mut self, global_state: &mut GlobalState) -> PlayStateResult {
|
||||||
|
|
||||||
|
// Set up an fps clock
|
||||||
|
let mut clock = Clock::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Handle window events
|
// Handle window events
|
||||||
for event in global_state.window.fetch_events() {
|
for event in global_state.window.fetch_events() {
|
||||||
match event {
|
match event {
|
||||||
Event::Close => return PlayStateResult::Shutdown,
|
Event::Close => return PlayStateResult::Shutdown,
|
||||||
// When space is pressed, start a session
|
// When space is pressed, go to the main menu
|
||||||
Event::Char(' ') => return PlayStateResult::Push(
|
Event::Char(' ') => return PlayStateResult::Push(
|
||||||
Box::new(SessionState::new(&mut global_state.window).unwrap()), // TODO: Handle this error
|
Box::new(MainMenuState::new(&mut global_state.window)),
|
||||||
),
|
),
|
||||||
// Pass events to ui
|
// Pass events to ui
|
||||||
Event::UiEvent(input) => {
|
Event::UiEvent(input) => {
|
||||||
@ -68,6 +72,9 @@ impl PlayState for TitleState {
|
|||||||
.swap_buffers()
|
.swap_buffers()
|
||||||
.expect("Failed to swap window buffers");
|
.expect("Failed to swap window buffers");
|
||||||
|
|
||||||
|
// Wait for the next tick
|
||||||
|
clock.tick(Duration::from_millis(1000 / FPS));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
|||||||
// Library
|
|
||||||
use conrod_core::{
|
use conrod_core::{
|
||||||
Positionable,
|
Positionable,
|
||||||
Widget,
|
Widget,
|
||||||
@ -9,8 +8,6 @@ use conrod_core::{
|
|||||||
Id as WidgId,
|
Id as WidgId,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Crate
|
|
||||||
use crate::{
|
use crate::{
|
||||||
window::Window,
|
window::Window,
|
||||||
render::Renderer,
|
render::Renderer,
|
||||||
@ -27,7 +24,7 @@ impl TitleUi {
|
|||||||
pub fn new(window: &mut Window) -> Self {
|
pub fn new(window: &mut Window) -> Self {
|
||||||
let mut ui = Ui::new(window).unwrap();
|
let mut ui = Ui::new(window).unwrap();
|
||||||
let widget_id = ui.id_generator().next();
|
let widget_id = ui.id_generator().next();
|
||||||
let image = image::open(concat!(env!("CARGO_MANIFEST_DIR"), "/test_assets/test.png")).unwrap();
|
let image = image::open(concat!(env!("CARGO_MANIFEST_DIR"), "/test_assets/ui/title/test.png")).unwrap();
|
||||||
let title_img_id = ui.new_image(window.renderer_mut(), &image).unwrap();
|
let title_img_id = ui.new_image(window.renderer_mut(), &image).unwrap();
|
||||||
Self {
|
Self {
|
||||||
ui,
|
ui,
|
@ -31,6 +31,7 @@ pub use self::{
|
|||||||
},
|
},
|
||||||
ui::{
|
ui::{
|
||||||
push_quad_to_mesh as push_ui_quad_to_mesh,
|
push_quad_to_mesh as push_ui_quad_to_mesh,
|
||||||
|
push_tri_to_mesh as push_ui_tri_to_mesh,
|
||||||
Mode as UiMode,
|
Mode as UiMode,
|
||||||
UiPipeline,
|
UiPipeline,
|
||||||
},
|
},
|
||||||
|
@ -16,6 +16,7 @@ use super::super::{
|
|||||||
TgtDepthFmt,
|
TgtDepthFmt,
|
||||||
Mesh,
|
Mesh,
|
||||||
Quad,
|
Quad,
|
||||||
|
Tri,
|
||||||
};
|
};
|
||||||
|
|
||||||
gfx_defines! {
|
gfx_defines! {
|
||||||
@ -87,3 +88,20 @@ pub fn push_quad_to_mesh(mesh: &mut Mesh<UiPipeline>, rect: [f32; 4], uv_rect:
|
|||||||
v([r, b], [uv_r, uv_b]),
|
v([r, b], [uv_r, uv_b]),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn push_tri_to_mesh(mesh: &mut Mesh<UiPipeline>, tri: [[f32; 2]; 3], uv_tri: [[f32; 2]; 3], color: [f32; 4], mode: Mode) {
|
||||||
|
let mode_val = mode.value();
|
||||||
|
let v = |pos, uv| {
|
||||||
|
Vertex {
|
||||||
|
pos,
|
||||||
|
uv,
|
||||||
|
color,
|
||||||
|
mode: mode_val,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mesh.push_tri(Tri::new(
|
||||||
|
v([tri[0][0], tri[0][1]], [uv_tri[0][0], uv_tri[0][1]]),
|
||||||
|
v([tri[1][0], tri[1][1]], [uv_tri[1][0], uv_tri[1][1]]),
|
||||||
|
v([tri[2][0], tri[2][1]], [uv_tri[2][0], uv_tri[2][1]]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
@ -31,6 +31,7 @@ use crate::{
|
|||||||
UiPipeline,
|
UiPipeline,
|
||||||
UiMode,
|
UiMode,
|
||||||
push_ui_quad_to_mesh,
|
push_ui_quad_to_mesh,
|
||||||
|
push_ui_tri_to_mesh,
|
||||||
},
|
},
|
||||||
window::Window,
|
window::Window,
|
||||||
};
|
};
|
||||||
@ -238,6 +239,9 @@ impl Ui {
|
|||||||
while let Some(prim) = primitives.next() {
|
while let Some(prim) = primitives.next() {
|
||||||
// TODO: Use scizzor
|
// TODO: Use scizzor
|
||||||
let Primitive {kind, scizzor, id, rect} = prim;
|
let Primitive {kind, scizzor, id, rect} = prim;
|
||||||
|
// Functions for converting for conrod scalar coords to GL vertex coords (-1.0 to 1.0)
|
||||||
|
let vx = |x: f64| (x / ui.win_w * 2.0) as f32;
|
||||||
|
let vy = |y: f64| (y / ui.win_h * 2.0) as f32;
|
||||||
|
|
||||||
use conrod_core::render::PrimitiveKind;
|
use conrod_core::render::PrimitiveKind;
|
||||||
match kind {
|
match kind {
|
||||||
@ -284,12 +288,7 @@ impl Ui {
|
|||||||
};
|
};
|
||||||
// Convert from conrod Scalar range to GL range -1.0 to 1.0.
|
// Convert from conrod Scalar range to GL range -1.0 to 1.0.
|
||||||
let (l, r, b, t) = rect.l_r_b_t();
|
let (l, r, b, t) = rect.l_r_b_t();
|
||||||
let (l, r, b, t) = (
|
let (l, r, b, t) = (vx(l), vx(r), vy(b), vy(t));
|
||||||
(l / ui.win_w * 2.0) as f32,
|
|
||||||
(r / ui.win_w * 2.0) as f32,
|
|
||||||
(b / ui.win_h * 2.0) as f32,
|
|
||||||
(t / ui.win_h * 2.0) as f32,
|
|
||||||
);
|
|
||||||
push_ui_quad_to_mesh(
|
push_ui_quad_to_mesh(
|
||||||
&mut mesh,
|
&mut mesh,
|
||||||
[l, t , r, b],
|
[l, t , r, b],
|
||||||
@ -349,12 +348,57 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PrimitiveKind::Rectangle { color } => {
|
||||||
|
// TODO: consider gamma/linear conversion....
|
||||||
|
let color = color.to_fsa();
|
||||||
|
// Don't draw a transparent rectangle (they exist)
|
||||||
|
if color[3] == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_to_plain_state!();
|
||||||
|
|
||||||
|
// Convert from conrod Scalar range to GL range -1.0 to 1.0.
|
||||||
|
let (l, r, b, t) = rect.l_r_b_t();
|
||||||
|
let (l, r, b, t) = (vx(l), vx(r), vy(b), vy(t));
|
||||||
|
push_ui_quad_to_mesh(
|
||||||
|
&mut mesh,
|
||||||
|
[l, t , r, b],
|
||||||
|
[0.0, 0.0, 0.0, 0.0],
|
||||||
|
color,
|
||||||
|
UiMode::Geometry,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PrimitiveKind::TrianglesSingleColor { color, triangles } => {
|
||||||
|
// Don't draw transparent triangle or switch state if there are actually no triangles
|
||||||
|
let color: [f32; 4] = color.into();
|
||||||
|
if triangles.is_empty() || color[3] == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_to_plain_state!();
|
||||||
|
|
||||||
|
for tri in triangles {
|
||||||
|
// TODO: this code is repeated above, put it in a single location
|
||||||
|
let triangle = [
|
||||||
|
[vx(tri[2][0]), vy(tri[2][1])],
|
||||||
|
[vx(tri[1][0]), vy(tri[1][1])],
|
||||||
|
[vx(tri[0][0]), vy(tri[0][1])],
|
||||||
|
];
|
||||||
|
push_ui_tri_to_mesh(
|
||||||
|
&mut mesh,
|
||||||
|
triangle,
|
||||||
|
[[0.0; 2]; 3],
|
||||||
|
[1.0, 1.0, 1.0, 1.0],
|
||||||
|
UiMode::Geometry,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
// TODO: Add these
|
// TODO: Add these
|
||||||
//PrimitiveKind::Other {..} => {println!("primitive kind other with id {:?}", id);}
|
//PrimitiveKind::Other {..} => {println!("primitive kind other with id {:?}", id);}
|
||||||
//PrimitiveKind::Rectangle { color } => {println!("primitive kind rect[x:{},y:{},w:{},h:{}] with color {:?} and id {:?}", x, y, w, h, color, id);}
|
|
||||||
//PrimitiveKind::TrianglesMultiColor {..} => {println!("primitive kind multicolor with id {:?}", id);}
|
//PrimitiveKind::TrianglesMultiColor {..} => {println!("primitive kind multicolor with id {:?}", id);}
|
||||||
//PrimitiveKind::TrianglesSingleColor {..} => {println!("primitive kind singlecolor with id {:?}", id);}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Enter the final command.
|
// Enter the final command.
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 606 B After Width: | Height: | Size: 606 B |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
BIN
voxygen/test_assets/ui/hud/bag/icon/0_bag.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
voxygen/test_assets/ui/hud/bag/icon/1_bag_hover.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
BIN
voxygen/test_assets/ui/hud/bag/icon/3_bag_open.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
voxygen/test_assets/ui/hud/bag/icon/4_bag_open_hover.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
voxygen/test_assets/ui/hud/bag/icon/5_bag_open_press.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
voxygen/test_assets/ui/hud/belt/belt_bg.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
voxygen/test_assets/ui/hud/belt/belt_bg_orig.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
voxygen/test_assets/ui/hud/belt/belt_grid.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
voxygen/test_assets/ui/hud/belt/belt_hover.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
voxygen/test_assets/ui/hud/belt/belt_open.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
voxygen/test_assets/ui/hud/belt/belt_press.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
1
voxygen/test_assets/ui/hud/mmap/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
BIN
voxygen/test_assets/ui/hud/mmap/mmap_frame.png
Normal file
After Width: | Height: | Size: 10 KiB |
1
voxygen/test_assets/ui/hud/skill_bar/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
BIN
voxygen/test_assets/ui/hud/skill_bar/sbar_bg.png
Normal file
After Width: | Height: | Size: 11 KiB |
1
voxygen/test_assets/ui/hud/x/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
1
voxygen/test_assets/ui/main/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
BIN
voxygen/test_assets/ui/main/bg.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
1
voxygen/test_assets/ui/main/buttons/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
BIN
voxygen/test_assets/ui/main/buttons/button.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
voxygen/test_assets/ui/main/buttons/button_hover.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
voxygen/test_assets/ui/main/buttons/button_login.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
voxygen/test_assets/ui/main/buttons/button_login_hover.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
voxygen/test_assets/ui/main/buttons/button_login_press.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
voxygen/test_assets/ui/main/buttons/button_press.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
5
voxygen/test_assets/ui/main/buttons/desktop.ini
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[.ShellClassInfo]
|
||||||
|
InfoTip=Dieser Ordner wird online freigegeben.
|
||||||
|
IconFile=C:\Program Files\Google\Drive\googledrivesync.exe
|
||||||
|
IconIndex=16
|
||||||
|
|
BIN
voxygen/test_assets/ui/main/input_bg.png
Normal file
After Width: | Height: | Size: 14 KiB |
1
voxygen/test_assets/ui/main/text/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
BIN
voxygen/test_assets/ui/main/text/login.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
voxygen/test_assets/ui/main/text/quit.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
voxygen/test_assets/ui/main/text/server_address.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
voxygen/test_assets/ui/main/text/servers.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
voxygen/test_assets/ui/main/text/settings.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
voxygen/test_assets/ui/main/text/username.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
voxygen/test_assets/ui/main/v_logo_a01.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
voxygen/test_assets/ui/main/v_logo_grey.png
Normal file
After Width: | Height: | Size: 41 KiB |
1
voxygen/test_assets/ui/title/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 4.4 MiB |