Add more parts of the character selection screen, add mouse detector widget, misc tweaks

This commit is contained in:
Imbris 2020-08-30 18:32:26 -04:00
parent de770694d8
commit 42c6550a0b
14 changed files with 749 additions and 233 deletions

View File

@ -108,6 +108,9 @@ Is the client up to date?"#,
/// Start Main screen section
"main.username": "Username",
"main.server": "Server",
"main.password": "Password",
"main.connecting": "Connecting",
"main.creating_world": "Creating world",
"main.tip": "Tip:",
@ -494,7 +497,7 @@ magically infused items?"#,
"char_selection.accessories": "Accessories",
"char_selection.create_info_name": "Your Character needs a name!",
/// End chracter selection section
/// End character selection section
/// Start character window section

View File

@ -37,13 +37,22 @@ impl CharSelectionState {
}
}
fn get_humanoid_body(&self, client: &Client) -> Option<comp::humanoid::Body> {
self.char_selection_ui
.display_character(&client.character_list.characters)
.and_then(|character| match character.body {
comp::Body::Humanoid(body) => Some(body),
_ => None,
fn get_humanoid_body_loadout<'a>(
char_selection_ui: &'a CharSelectionUi,
client: &'a Client,
) -> (Option<comp::humanoid::Body>, Option<&'a comp::Loadout>) {
char_selection_ui
.display_body_loadout(&client.character_list.characters)
.map(|(body, loadout)| {
(
match body {
comp::Body::Humanoid(body) => Some(body),
_ => None,
},
Some(loadout),
)
})
.unwrap_or_default()
}
}
@ -87,7 +96,9 @@ impl PlayState for CharSelectionState {
return PlayStateResult::Pop;
},
ui::Event::AddCharacter { alias, tool, body } => {
self.client.borrow_mut().create_character(alias, tool, body);
self.client
.borrow_mut()
.create_character(alias, Some(tool), body);
},
ui::Event::DeleteCharacter(character_id) => {
self.client.borrow_mut().delete_character(character_id);
@ -103,13 +114,11 @@ impl PlayState for CharSelectionState {
}
}
let loadout = self.char_selection_ui.get_loadout();
// Maintain the scene.
{
let client = self.client.borrow();
let humanoid_body = self.get_humanoid_body(&client);
let loadout = self.char_selection_ui.get_loadout();
let (humanoid_body, loadout) =
Self::get_humanoid_body_loadout(&self.char_selection_ui, &client);
// Maintain the scene.
let scene_data = scene::SceneData {
@ -128,11 +137,8 @@ impl PlayState for CharSelectionState {
as f32,
};
self.scene.maintain(
global_state.window.renderer_mut(),
scene_data,
loadout.as_ref(),
);
self.scene
.maintain(global_state.window.renderer_mut(), scene_data, loadout);
}
// Tick the client (currently only to keep the connection alive).
@ -187,12 +193,12 @@ impl PlayState for CharSelectionState {
fn render(&mut self, renderer: &mut Renderer, _: &Settings) {
let client = self.client.borrow();
let humanoid_body = self.get_humanoid_body(&client);
let loadout = self.char_selection_ui.get_loadout();
let (humanoid_body, loadout) =
Self::get_humanoid_body_loadout(&self.char_selection_ui, &client);
// Render the scene.
self.scene
.render(renderer, client.get_tick(), humanoid_body, loadout.as_ref());
.render(renderer, client.get_tick(), humanoid_body, loadout);
// Draw the UI to the screen.
self.char_selection_ui.render(renderer);

View File

@ -7,8 +7,11 @@ use crate::{
ice::{
component::neat_button,
style,
widget::{AspectRatioContainer, Overlay},
Element, IcedUi as Ui,
widget::{
mouse_detector, AspectRatioContainer, BackgroundContainer, Image, MouseDetector,
Overlay, Padding,
},
Element, IcedRenderer, IcedUi as Ui,
},
img_ids::{ImageGraphic, VoxelGraphic},
},
@ -19,6 +22,7 @@ use common::{
assets::Asset,
character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER},
comp::{self, humanoid},
LoadoutBuilder,
};
//ImageFrame, Tooltip,
use crate::settings::Settings;
@ -26,7 +30,7 @@ use crate::settings::Settings;
//use ui::ice::widget;
use iced::{
button, scrollable, text_input, Align, Button, Column, Container, HorizontalAlignment, Length,
Row, Scrollable, Space, Text,
Row, Scrollable, Space, Text, TextInput,
};
use vek::Rgba;
@ -40,17 +44,17 @@ const STARTER_BOW: &str = "common.items.weapons.bow.starter_bow";
const STARTER_AXE: &str = "common.items.weapons.axe.starter_axe";
const STARTER_STAFF: &str = "common.items.weapons.staff.starter_staff";
const STARTER_SWORD: &str = "common.items.weapons.sword.starter_sword";
const STARTER_DAGGER: &str = "common.items.weapons.dagger.starter_dagger";
// TODO: look into what was using this in old ui
const UI_MAIN: iced::Color = iced::Color::from_rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue
image_ids_ice! {
struct Imgs {
<VoxelGraphic>
<ImageGraphic>
slider_range: "voxygen.element.slider.track",
slider_indicator: "voxygen.element.slider.indicator",
<ImageGraphic>
gray_corner: "voxygen.element.frames.gray.corner",
gray_edge: "voxygen.element.frames.gray.edge",
@ -73,8 +77,6 @@ image_ids_ice! {
staff: "voxygen.element.icons.staff",
// Species Icons
male: "voxygen.element.icons.male",
female: "voxygen.element.icons.female",
human_m: "voxygen.element.icons.human_m",
human_f: "voxygen.element.icons.human_f",
orc_m: "voxygen.element.icons.orc_m",
@ -115,7 +117,7 @@ pub enum Event {
Play(CharacterId),
AddCharacter {
alias: String,
tool: Option<String>,
tool: String,
body: comp::Body,
},
DeleteCharacter(CharacterId),
@ -123,6 +125,7 @@ pub enum Event {
enum Mode {
Select {
info_content: Option<InfoContent>,
// Index of selected character
selected: Option<usize>,
@ -132,13 +135,20 @@ enum Mode {
logout_button: button::State,
enter_world_button: button::State,
change_server_button: button::State,
yes_button: button::State,
no_button: button::State,
},
Create {
name: String, // TODO: default to username
body: humanoid::Body,
loadout: comp::Loadout,
tool: Option<&'static str>,
// TODO: does this need to be an option, never seems to be none
tool: &'static str,
body_type_buttons: [button::State; 2],
species_buttons: [button::State; 6],
tool_buttons: [button::State; 6],
scroll: scrollable::State,
name_input: text_input::State,
back_button: button::State,
create_button: button::State,
@ -148,6 +158,7 @@ enum Mode {
impl Mode {
pub fn select() -> Self {
Self::Select {
info_content: None,
selected: None,
characters_scroll: Default::default(),
character_buttons: Vec::new(),
@ -155,16 +166,29 @@ impl Mode {
logout_button: Default::default(),
enter_world_button: Default::default(),
change_server_button: Default::default(),
yes_button: Default::default(),
no_button: Default::default(),
}
}
pub fn create(name: String) -> Self {
let tool = STARTER_SWORD;
let loadout = LoadoutBuilder::new()
.defaults()
.active_item(Some(LoadoutBuilder::default_item_config_from_str(tool)))
.build();
Self::Create {
name,
body: humanoid::Body::random(),
loadout: comp::Loadout::default(),
tool: Some(STARTER_SWORD),
loadout,
tool,
body_type_buttons: Default::default(),
species_buttons: Default::default(),
tool_buttons: Default::default(),
scroll: Default::default(),
name_input: Default::default(),
back_button: Default::default(),
create_button: Default::default(),
@ -204,7 +228,8 @@ struct Controls {
// Alpha disclaimer
alpha: String,
info_content: Option<InfoContent>,
// Zone for rotating the character with the mouse
mouse_detector: mouse_detector::State,
// enter: bool,
mode: Mode,
}
@ -214,11 +239,17 @@ enum Message {
Back,
Logout,
EnterWorld,
Select(usize),
Delete(usize),
ChangeServer,
NewCharacter,
CreateCharacter,
Name(String),
BodyType(humanoid::BodyType),
Species(humanoid::Species),
Tool(&'static str),
CancelDeletion,
ConfirmDeletion,
}
impl Controls {
@ -237,14 +268,15 @@ impl Controls {
version,
alpha,
info_content: None,
mouse_detector: Default::default(),
mode: Mode::select(),
}
}
fn view(&mut self, settings: &Settings, client: &Client) -> Element<Message> {
// TODO: if enter key pressed and character is selected then enter the world
// TODO: tooltip widget
// TODO: use font scale thing for text size (use on button size for buttons with
// text) TODO: if enter key pressed and character is selected then enter
// the world TODO: tooltip widget
let imgs = &self.imgs;
let fonts = &self.fonts;
@ -275,6 +307,7 @@ impl Controls {
let content = match &mut self.mode {
Mode::Select {
info_content,
selected,
ref mut characters_scroll,
ref mut character_buttons,
@ -282,14 +315,14 @@ impl Controls {
ref mut logout_button,
ref mut enter_world_button,
ref mut change_server_button,
ref mut yes_button,
ref mut no_button,
} => {
// TODO: impl delete prompt as overlay
let server = Container::new(
Column::with_children(vec![
Text::new(&client.server_info.name)
.size(fonts.cyri.scale(25))
//.horizontal_alignment(HorizontalAlignment::Center)
.into(),
.size(fonts.cyri.scale(25))
.into(),
Container::new(neat_button(
change_server_button,
i18n.get("char_selection.change_server"),
@ -333,18 +366,19 @@ impl Controls {
Overlay::new(
Button::new(
select_button,
Space::new(Length::Units(20), Length::Units(20)),
Space::new(Length::Units(16), Length::Units(16)),
)
.style(
style::button::Style::new(imgs.delete_button)
.hover_image(imgs.delete_button_hover)
.press_image(imgs.delete_button_press),
),
)
.on_press(Message::Delete(i)),
AspectRatioContainer::new(
Button::new(
delete_button,
Column::with_children(vec![
Text::new("Hi").width(Length::Fill).into(),
Text::new("Hi").into(),
Text::new("Hi").into(),
Text::new("Hi").into(),
]),
@ -353,11 +387,14 @@ impl Controls {
style::button::Style::new(imgs.selection)
.hover_image(imgs.selection_hover)
.press_image(imgs.selection_press),
),
)
.width(Length::Fill)
.height(Length::Fill)
.on_press(Message::Select(i)),
)
.ratio_of_image(imgs.selection),
)
.padding(15)
.padding(12)
.align_x(Align::End)
.into()
})
@ -393,7 +430,6 @@ impl Controls {
)
.width(Length::Fill)
.height(Length::Fill);
// TODO: try to get better interface for this in iced
if num < MAX_CHARACTERS_PER_PLAYER {
button.on_press(Message::NewCharacter)
} else {
@ -410,7 +446,7 @@ impl Controls {
// children method
let characters = Container::new(
Scrollable::new(characters_scroll)
.push(Column::with_children(characters).spacing(2)),
.push(Column::with_children(characters).spacing(4)),
)
.style(style::container::Style::color_with_image_border(
Rgba::new(0, 0, 0, 217),
@ -424,14 +460,19 @@ impl Controls {
let right_column = Column::with_children(vec![server.into(), characters.into()])
.padding(15)
.spacing(10)
.width(Length::Fill)
.height(Length::Fill)
.max_width(360);
let top = Container::new(right_column)
.width(Length::Fill)
.width(Length::Units(360)) // TODO: see if we can get iced to work with settings below
//.max_width(360)
//.width(Length::Fill)
.height(Length::Fill);
let top = Row::with_children(vec![
right_column.into(),
MouseDetector::new(&mut self.mouse_detector, Length::Fill, Length::Fill).into(),
])
.padding(15)
.width(Length::Fill)
.height(Length::Fill);
let logout = neat_button(
logout_button,
i18n.get("char_selection.logout"),
@ -462,30 +503,359 @@ impl Controls {
])
.align_items(Align::End);
Column::with_children(vec![top.into(), bottom.into()])
let content = Column::with_children(vec![top.into(), bottom.into()])
.width(Length::Fill)
.height(Length::Fill)
.height(Length::Fill);
// Overlay delete prompt
if let Some(info_content) = info_content {
let over: Element<_> = match info_content {
InfoContent::Deletion(_) => Container::new(
Column::with_children(vec![
Text::new(self.i18n.get("char_selection.delete_permanently"))
.size(fonts.cyri.scale(24))
.into(),
Row::with_children(vec![
neat_button(
no_button,
i18n.get("common.no"),
FILL_FRAC_ONE,
button_style,
Some(Message::CancelDeletion),
),
neat_button(
yes_button,
i18n.get("common.yes"),
FILL_FRAC_ONE,
button_style,
Some(Message::ConfirmDeletion),
),
])
.height(Length::Units(28))
.spacing(30)
.into(),
])
.align_items(Align::Center)
.spacing(10),
)
.style(
style::container::Style::color_with_double_cornerless_border(
(0, 0, 0, 200).into(),
(3, 4, 4, 255).into(),
(28, 28, 22, 255).into(),
),
)
.width(Length::Fill)
.height(Length::Fill)
.max_width(400)
.max_height(130)
.padding(16)
.center_x()
.center_y()
.into(),
// TODO
_ => Space::new(Length::Shrink, Length::Shrink).into(),
};
Overlay::new(over, content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
} else {
content.into()
}
},
Mode::Create {
name,
body,
loadout,
tool,
name_input,
back_button,
create_button,
ref mut scroll,
ref mut body_type_buttons,
ref mut species_buttons,
ref mut tool_buttons,
ref mut name_input,
ref mut back_button,
ref mut create_button,
} => {
let top_row = Row::with_children(vec![]);
let bottom_row = Row::with_children(vec![]);
let unselected_style = style::button::Style::new(imgs.icon_border)
.hover_image(imgs.icon_border_mo)
.press_image(imgs.icon_border_press);
Column::with_children(vec![top_row.into(), bottom_row.into()])
let selected_style = style::button::Style::new(imgs.icon_border_pressed)
.hover_image(imgs.icon_border_mo)
.press_image(imgs.icon_border_press);
let icon_button = |button, selected, msg, img| {
Container::new(
Button::<_, IcedRenderer>::new(
button,
Space::new(Length::Units(70), Length::Units(70)),
)
.style(if selected {
selected_style
} else {
unselected_style
})
.on_press(msg),
)
.style(style::container::Style::image(img))
};
let (body_m_ico, body_f_ico) = match body.species {
humanoid::Species::Human => (imgs.human_m, imgs.human_f),
humanoid::Species::Orc => (imgs.orc_m, imgs.orc_f),
humanoid::Species::Dwarf => (imgs.dwarf_m, imgs.dwarf_f),
humanoid::Species::Elf => (imgs.elf_m, imgs.elf_f),
humanoid::Species::Undead => (imgs.undead_m, imgs.undead_f),
humanoid::Species::Danari => (imgs.danari_m, imgs.danari_f),
};
let [ref mut body_m_button, ref mut body_f_button] = body_type_buttons;
let body_type = Row::with_children(vec![
icon_button(
body_m_button,
matches!(body.body_type, humanoid::BodyType::Male),
Message::BodyType(humanoid::BodyType::Male),
body_m_ico,
)
.into(),
icon_button(
body_f_button,
matches!(body.body_type, humanoid::BodyType::Female),
Message::BodyType(humanoid::BodyType::Female),
body_f_ico,
)
.into(),
])
.spacing(1);
let (human_icon, orc_icon, dwarf_icon, elf_icon, undead_icon, danari_icon) =
match body.body_type {
humanoid::BodyType::Male => (
self.imgs.human_m,
self.imgs.orc_m,
self.imgs.dwarf_m,
self.imgs.elf_m,
self.imgs.undead_m,
self.imgs.danari_m,
),
humanoid::BodyType::Female => (
self.imgs.human_f,
self.imgs.orc_f,
self.imgs.dwarf_f,
self.imgs.elf_f,
self.imgs.undead_f,
self.imgs.danari_f,
),
};
// TODO: tooltips
let [ref mut human_button, ref mut orc_button, ref mut dwarf_button, ref mut elf_button, ref mut undead_button, ref mut danari_button] =
species_buttons;
let species = Column::with_children(vec![
Row::with_children(vec![
icon_button(
human_button,
matches!(body.species, humanoid::Species::Human),
Message::Species(humanoid::Species::Human),
human_icon,
)
.into(),
icon_button(
orc_button,
matches!(body.species, humanoid::Species::Orc),
Message::Species(humanoid::Species::Orc),
orc_icon,
)
.into(),
icon_button(
dwarf_button,
matches!(body.species, humanoid::Species::Dwarf),
Message::Species(humanoid::Species::Dwarf),
dwarf_icon,
)
.into(),
])
.spacing(1)
.into(),
Row::with_children(vec![
icon_button(
elf_button,
matches!(body.species, humanoid::Species::Elf),
Message::Species(humanoid::Species::Elf),
elf_icon,
)
.into(),
icon_button(
undead_button,
matches!(body.species, humanoid::Species::Undead),
Message::Species(humanoid::Species::Undead),
undead_icon,
)
.into(),
icon_button(
danari_button,
matches!(body.species, humanoid::Species::Danari),
Message::Species(humanoid::Species::Danari),
danari_icon,
)
.into(),
])
.spacing(1)
.into(),
])
.spacing(1);
let [ref mut sword_button, ref mut daggers_button, ref mut axe_button, ref mut hammer_button, ref mut bow_button, ref mut staff_button] =
tool_buttons;
let tool = Column::with_children(vec![
Row::with_children(vec![
icon_button(
sword_button,
*tool == STARTER_SWORD,
Message::Tool(STARTER_SWORD),
imgs.sword,
)
.into(),
icon_button(
daggers_button,
*tool == STARTER_DAGGER,
Message::Tool(STARTER_DAGGER),
imgs.daggers,
)
.into(),
icon_button(
axe_button,
*tool == STARTER_AXE,
Message::Tool(STARTER_AXE),
imgs.axe,
)
.into(),
])
.spacing(1)
.into(),
Row::with_children(vec![
icon_button(
hammer_button,
*tool == STARTER_HAMMER,
Message::Tool(STARTER_HAMMER),
imgs.hammer,
)
.into(),
icon_button(
bow_button,
*tool == STARTER_BOW,
Message::Tool(STARTER_BOW),
imgs.bow,
)
.into(),
icon_button(
staff_button,
*tool == STARTER_STAFF,
Message::Tool(STARTER_STAFF),
imgs.staff,
)
.into(),
])
.spacing(1)
.into(),
])
.spacing(1);
let column_content = vec![body_type.into(), species.into(), tool.into()];
let right_column = Container::new(
Column::with_children(vec![
Text::new(i18n.get("char_selection.character_creation"))
.size(fonts.cyri.scale(26))
.into(),
Scrollable::new(scroll)
.push(
Column::with_children(column_content)
.align_items(Align::Center)
.spacing(16),
)
.into(),
])
.spacing(20)
.padding(10)
.width(Length::Fill)
.align_items(Align::Center),
)
.style(style::container::Style::color_with_image_border(
Rgba::new(0, 0, 0, 217),
imgs.gray_corner,
imgs.gray_edge,
))
.padding(10)
.width(Length::Units(360)) // TODO: see if we can get iced to work with settings below
//.max_width(360)
//.width(Length::Fill)
.height(Length::Fill);
let top = Row::with_children(vec![
right_column.into(),
MouseDetector::new(&mut self.mouse_detector, Length::Fill, Length::Fill).into(),
])
.padding(15)
.width(Length::Fill)
.height(Length::Fill);
let back = neat_button(
back_button,
i18n.get("common.back"),
FILL_FRAC_ONE,
button_style,
Some(Message::Back),
);
let name_input = BackgroundContainer::new(
Image::new(imgs.name_input)
.height(Length::Units(40))
.fix_aspect_ratio(),
TextInput::new(name_input, "Character Name", &name, Message::Name)
.size(25)
.on_submit(Message::CreateCharacter),
)
.padding(Padding::new().horizontal(7).top(5));
let create = neat_button(
create_button,
i18n.get("common.create"),
FILL_FRAC_ONE,
button_style,
(!name.is_empty()).then_some(Message::CreateCharacter),
);
let bottom = Row::with_children(vec![
Container::new(back)
.width(Length::Fill)
.height(Length::Units(40))
.into(),
Container::new(name_input)
.width(Length::Fill)
.center_x()
.into(),
Container::new(create)
.width(Length::Fill)
.height(Length::Units(40))
.align_x(Align::End)
.into(),
])
.align_items(Align::End);
Column::with_children(vec![top.into(), bottom.into()])
.width(Length::Fill)
.height(Length::Fill)
.into()
},
};
Container::new(
Column::with_children(vec![top_text.into(), content.into()])
Column::with_children(vec![top_text.into(), content])
.spacing(3)
.width(Length::Fill)
.height(Length::Fill),
@ -494,15 +864,7 @@ impl Controls {
.into()
}
fn update(
&mut self,
message: Message,
events: &mut Vec<Event>,
settings: &Settings,
characters: &[CharacterItem],
) {
let servers = &settings.networking.servers;
fn update(&mut self, message: Message, events: &mut Vec<Event>, characters: &[CharacterItem]) {
match message {
Message::Back => {
if matches!(&self.mode, Mode::Create { .. }) {
@ -518,15 +880,20 @@ impl Controls {
..
} = &self.mode
{
// TODO: eliminate option in character id
// TODO: eliminate option in character id?
if let Some(id) = characters.get(*selected).and_then(|i| i.character.id) {
events.push(Event::Play(id));
}
}
},
Message::Select(idx) => {
if let Mode::Select { selected, .. } = &mut self.mode {
*selected = Some(idx);
}
},
Message::Delete(idx) => {
if let Some(id) = characters.get(idx).and_then(|i| i.character.id) {
events.push(Event::DeleteCharacter(id));
if let Mode::Select { info_content, .. } = &mut self.mode {
*info_content = Some(InfoContent::Deletion(idx));
}
},
Message::ChangeServer => {
@ -544,7 +911,7 @@ impl Controls {
{
events.push(Event::AddCharacter {
alias: name.clone(),
tool: tool.map(String::from),
tool: String::from(*tool),
body: comp::Body::Humanoid(*body),
});
self.mode = Mode::select();
@ -555,16 +922,54 @@ impl Controls {
*name = value;
}
},
Message::BodyType(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
body.body_type = value;
body.validate();
}
},
Message::Species(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
body.species = value;
body.validate();
}
},
Message::Tool(value) => {
if let Mode::Create { tool, loadout, .. } = &mut self.mode {
*tool = value;
loadout.active_item = Some(LoadoutBuilder::default_item_config_from_str(*tool));
}
},
Message::ConfirmDeletion => {
if let Mode::Select { info_content, .. } = &mut self.mode {
if let Some(InfoContent::Deletion(idx)) = info_content {
if let Some(id) = characters.get(*idx).and_then(|i| i.character.id) {
events.push(Event::DeleteCharacter(id));
}
*info_content = None;
}
}
},
Message::CancelDeletion => {
if let Mode::Select { info_content, .. } = &mut self.mode {
if let Some(InfoContent::Deletion(idx)) = info_content {
*info_content = None;
}
}
},
}
}
/// Get the character to display
pub fn display_character(&self, characters: &[CharacterItem]) -> Option<&CharacterItem> {
match self.mode {
// TODO
Mode::Select { .. } => None,
// TODO
Mode::Create { .. } => None,
pub fn display_body_loadout<'a>(
&'a self,
characters: &'a [CharacterItem],
) -> Option<(comp::Body, &'a comp::Loadout)> {
match &self.mode {
Mode::Select { selected, .. } => selected
.and_then(|idx| characters.get(idx))
.map(|i| (i.body, &i.loadout)),
Mode::Create { loadout, body, .. } => Some((comp::Body::Humanoid(*body), loadout)),
}
}
}
@ -607,44 +1012,11 @@ impl CharSelectionUi {
Self { ui, controls }
}
pub fn display_character(&self, characters: &[CharacterItem]) -> Option<&CharacterItem> {
self.controls.display_character(characters)
}
// TODO
pub fn get_loadout(&mut self) -> Option<comp::Loadout> {
// TODO: error gracefully
// TODO: don't clone
/*match &mut self.mode {
Mode::Select(character_list) => {
if let Some(data) = character_list {
data.get(self.selected_character).map(|c| c.loadout.clone())
} else {
None
}
},
Mode::Create { loadout, tool, .. } => {
loadout.active_item = tool.map(|tool| comp::ItemConfig {
item: comp::Item::new_from_asset_expect(tool),
ability1: None,
ability2: None,
ability3: None,
block_ability: None,
dodge_ability: None,
});
loadout.chest = Some(comp::Item::new_from_asset_expect(
"common.items.armor.starter.rugged_chest",
));
loadout.pants = Some(comp::Item::new_from_asset_expect(
"common.items.armor.starter.rugged_pants",
));
loadout.foot = Some(comp::Item::new_from_asset_expect(
"common.items.armor.starter.sandals_0",
));
Some(loadout.clone())
},
}*/
None
pub fn display_body_loadout<'a>(
&'a self,
characters: &'a [CharacterItem],
) -> Option<(comp::Body, &'a comp::Loadout)> {
self.controls.display_body_loadout(characters)
}
pub fn handle_event(&mut self, event: window::Event) -> bool {
@ -654,15 +1026,13 @@ impl CharSelectionUi {
true
},
window::Event::MouseButton(_, window::PressState::Pressed) => {
// TODO: implement this with iced
// !self.ui.no_widget_capturing_mouse()
false
!self.controls.mouse_detector.mouse_over()
},
_ => false,
}
}
// TODO: do we need whole client here or just character list
// TODO: do we need whole client here or just character list?
pub fn maintain(&mut self, global_state: &mut GlobalState, client: &mut Client) -> Vec<Event> {
let mut events = Vec::new();
@ -672,17 +1042,13 @@ impl CharSelectionUi {
);
messages.into_iter().for_each(|message| {
self.controls.update(
message,
&mut events,
&global_state.settings,
&client.character_list.characters,
)
self.controls
.update(message, &mut events, &client.character_list.characters)
});
events
}
// TODO: do we need globals
// TODO: do we need globals?
pub fn render(&self, renderer: &mut Renderer) { self.ui.render(renderer); }
}

View File

@ -10,7 +10,7 @@ use crate::{
};
use client_init::{ClientInit, Error as InitError, Msg as InitMsg};
use common::{assets::Asset, comp, span};
use tracing::{error, warn};
use tracing::error;
use ui::{Event as MainMenuEvent, MainMenuUi};
pub struct MainMenuState {
@ -221,12 +221,11 @@ impl PlayState for MainMenuState {
} => {
let mut net_settings = &mut global_state.settings.networking;
net_settings.username = username.clone();
net_settings.default_server = server_address.clone();
if !net_settings.servers.contains(&server_address) {
net_settings.servers.push(server_address.clone());
}
if let Err(err) = global_state.settings.save_to_file() {
warn!("Failed to save settings: {:?}", err);
}
global_state.settings.save_to_file_warn();
attempt_login(
&mut global_state.settings,

View File

@ -202,6 +202,7 @@ impl Banner {
.padding(10)
.height(Length::FillPortion(25))
.into(),
// TODO: i18n
Column::with_children(vec![
BackgroundContainer::new(
Image::new(imgs.input_bg)
@ -209,7 +210,7 @@ impl Banner {
.fix_aspect_ratio(),
TextInput::new(
&mut self.username,
"Username",
i18n.get("main.username"),
&login_info.username,
Message::Username,
)
@ -224,7 +225,7 @@ impl Banner {
.fix_aspect_ratio(),
TextInput::new(
&mut self.password,
"Password",
i18n.get("main.password"),
&login_info.password,
Message::Password,
)
@ -240,7 +241,7 @@ impl Banner {
.fix_aspect_ratio(),
TextInput::new(
&mut self.server,
"Server",
i18n.get("main.server"),
&login_info.server,
Message::Server,
)

View File

@ -266,7 +266,8 @@ impl IcedRenderer {
let mesh_index = *mesh_index;
let linear_color = *linear_color;
// Could potentially pass this in as part of the extras
let offset = offset.map(|e| e as f32 * p_scale) / half_res;
let offset =
offset.map(|e| e as f32 * p_scale) / half_res * Vec2::new(-1.0, 1.0);
(0..*glyph_count).map(move |i| (mesh_index + i * 6, linear_color, offset))
})
.zip(self.last_glyph_verts.iter())
@ -371,6 +372,68 @@ impl IcedRenderer {
Aabr { min, max }
}
fn position_glyphs(
&mut self,
bounds: iced::Rectangle,
horizontal_alignment: iced::HorizontalAlignment,
vertical_alignment: iced::VerticalAlignment,
text: &str,
size: u16,
font: FontId,
) -> Vec<glyph_brush::SectionGlyph> {
use glyph_brush::{GlyphCruncher, HorizontalAlign, VerticalAlign};
// TODO: add option to align based on the geometry of the rendered glyphs
// instead of all possible glyphs
let (x, h_align) = match horizontal_alignment {
iced::HorizontalAlignment::Left => (bounds.x, HorizontalAlign::Left),
iced::HorizontalAlignment::Center => (bounds.center_x(), HorizontalAlign::Center),
iced::HorizontalAlignment::Right => (bounds.x + bounds.width, HorizontalAlign::Right),
};
let (y, v_align) = match vertical_alignment {
iced::VerticalAlignment::Top => (bounds.y, VerticalAlign::Top),
iced::VerticalAlignment::Center => (bounds.center_y(), VerticalAlign::Center),
iced::VerticalAlignment::Bottom => (bounds.y + bounds.height, VerticalAlign::Bottom),
};
let p_scale = self.p_scale;
let section = glyph_brush::Section {
screen_position: (x * p_scale, y * p_scale),
bounds: (bounds.width * p_scale, bounds.height * p_scale),
layout: glyph_brush::Layout::Wrap {
line_breaker: Default::default(),
h_align,
v_align,
},
text: vec![glyph_brush::Text {
text,
scale: (size as f32 * p_scale).into(),
font_id: font.0,
extra: (),
}],
};
self
.cache
.glyph_cache_mut()
.glyphs(section)
// We would still have to generate vertices for these even if they have no pixels
// Note: this is somewhat hacky and could fail if there is a non-whitespace character
// that is not visible (to solve this we could use the extra values in
// queue_pre_positioned to keep track of which glyphs are actually returned by
// proccess_queued)
.filter(|g| {
!text[g.byte_index..]
.chars()
.next()
.unwrap()
.is_whitespace()
})
.cloned()
.collect()
}
fn draw_primitive(&mut self, primitive: Primitive, offset: Vec2<u32>, renderer: &mut Renderer) {
match primitive {
Primitive::Group { primitives } => {
@ -391,8 +454,8 @@ impl IcedRenderer {
let (graphic_id, rotation) = handle;
let gl_aabr = self.gl_aabr(iced::Rectangle {
x: bounds.x + offset.x as f32,
y: bounds.y + offset.y as f32,
x: bounds.x - offset.x as f32,
y: bounds.y - offset.y as f32,
..bounds
});
@ -468,8 +531,8 @@ impl IcedRenderer {
self.switch_state(State::Plain);
let gl_aabr = self.gl_aabr(iced::Rectangle {
x: bounds.x + offset.x as f32,
y: bounds.y + offset.y as f32,
x: bounds.x - offset.x as f32,
y: bounds.y - offset.y as f32,
..bounds
});
@ -497,8 +560,8 @@ impl IcedRenderer {
self.switch_state(State::Plain);
let gl_aabr = self.gl_aabr(iced::Rectangle {
x: bounds.x + offset.x as f32,
y: bounds.y + offset.y as f32,
x: bounds.x - offset.x as f32,
y: bounds.y - offset.y as f32,
..bounds
});

View File

@ -5,6 +5,7 @@ mod column;
mod compound_graphic;
mod container;
mod image;
mod mouse_detector;
mod overlay;
mod row;
mod scrollable;

View File

@ -0,0 +1,9 @@
use super::super::{super::widget::mouse_detector, IcedRenderer, Primitive};
use iced::{mouse, Rectangle};
impl mouse_detector::Renderer for IcedRenderer {
fn draw(&mut self, _bounds: Rectangle) -> Self::Output {
// TODO: mouse interaction if in bounds??
(Primitive::Nothing, mouse::Interaction::default())
}
}

View File

@ -42,54 +42,14 @@ impl text::Renderer for IcedRenderer {
horizontal_alignment: HorizontalAlignment,
vertical_alignment: VerticalAlignment,
) -> Self::Output {
use glyph_brush::{HorizontalAlign, VerticalAlign};
// glyph_brush thought it would be a great idea to change what the bounds and
// position mean based on the alignment
// TODO: add option to align based on the geometry of the rendered glyphs
// instead of all possible glyphs
let (x, h_align) = match horizontal_alignment {
HorizontalAlignment::Left => (bounds.x, HorizontalAlign::Left),
HorizontalAlignment::Center => (bounds.center_x(), HorizontalAlign::Center),
HorizontalAlignment::Right => (bounds.x + bounds.width, HorizontalAlign::Right),
};
let (y, v_align) = match vertical_alignment {
VerticalAlignment::Top => (bounds.y, VerticalAlign::Top),
VerticalAlignment::Center => (bounds.center_y(), VerticalAlign::Center),
VerticalAlignment::Bottom => (bounds.y + bounds.height, VerticalAlign::Bottom),
};
let p_scale = self.p_scale;
let section = glyph_brush::Section {
screen_position: (x * p_scale, y * p_scale),
bounds: (bounds.width * p_scale, bounds.height * p_scale),
layout: glyph_brush::Layout::Wrap {
line_breaker: Default::default(),
h_align,
v_align,
},
text: vec![glyph_brush::Text {
text: content,
scale: (size as f32 * p_scale).into(),
font_id: font.0,
extra: (),
}],
};
let glyphs = self
.cache
.glyph_cache_mut()
.glyphs(section)
.filter(|g| {
!content[g.byte_index..]
.chars()
.next()
.unwrap()
.is_whitespace()
})
.cloned()
.collect::<Vec<_>>();
let glyphs = self.position_glyphs(
bounds,
horizontal_alignment,
vertical_alignment,
content,
size,
font,
);
(
Primitive::Text {

View File

@ -60,7 +60,7 @@ impl text_input::Renderer for IcedRenderer {
font_id: font.0,
extra: (),
}],
};
i;
let font = glyph_calculator.fonts()[font.0].as_scaled(size as f32);
let space_id = font.glyph_id(' ');
let x_id = font.glyph_id('x');
@ -215,9 +215,22 @@ impl text_input::Renderer for IcedRenderer {
};
let display_text = text.unwrap_or(if state.is_focused() { "" } else { placeholder });
let section = glyph_brush::Section {
// Note: clip offset is an integer so we don't have to worry about not accounting for
// that here where the alignment of the glyphs with pixels affects rasterization
// Note: clip offset is an integer so we don't have to worry about not
// accounting for that here where the alignment of the glyphs with
// pixels affects rasterization
let glyphs = self.position_glyphs(
Rectangle {
width: 1000.0, // hacky
..bounds
},
iced::HorizontalAlignment::Left,
iced::VerticalAlignment::Center,
display_text,
size,
font,
);
// TODO: delete if new arrangment behaves nicely
/*let section = glyph_brush::Section {
screen_position: (text_bounds.x * p_scale, text_bounds.center_y() * p_scale),
bounds: (
10000.0, /* text_bounds.width * p_scale */
@ -234,26 +247,7 @@ impl text_input::Renderer for IcedRenderer {
font_id: font.0,
extra: (),
}],
};
let glyphs = self
.cache
.glyph_cache_mut()
.glyphs(section)
// We would still have to generate vertices for these even if they have no pixels
// Note: this is somewhat hacky and could fail if there is a non-whitespace character
// that is not visible (to solve this we could use the extra values in
// queue_pre_positioned to keep track of which glyphs are actually returned by
// proccess_queued)
.filter(|g| {
!display_text[g.byte_index..]
.chars()
.next()
.unwrap()
.is_whitespace()
})
.cloned()
.collect::<Vec<_>>();
};*/
let text_primitive = Primitive::Text {
glyphs,

View File

@ -1,4 +1,6 @@
use iced::{layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Widget};
use iced::{
layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
};
use std::{hash::Hash, u32};
// Note: it might be more efficient to make this generic over the content type?
@ -70,17 +72,15 @@ impl<'a, M, R> Widget<M, R> for AspectRatioContainer<'a, M, R>
where
R: self::Renderer,
{
fn width(&self) -> Length { Length::Fill }
fn width(&self) -> Length { Length::Shrink }
fn height(&self) -> Length { Length::Fill }
fn height(&self) -> Length { Length::Shrink }
fn layout(&self, renderer: &R, limits: &layout::Limits) -> layout::Node {
let limits = limits
.loose()
.max_width(self.max_width)
.max_height(self.max_height)
.width(self.width())
.height(self.height());
.max_height(self.max_height);
let aspect_ratio = match &self.aspect_ratio {
AspectRatio::Image(handle) => {
@ -107,7 +107,9 @@ where
limits.max_height((max_width / aspect_ratio) as u32)
};
let content = self.content.layout(renderer, &limits.loose());
// Remove fill limits in case one of the parents was Shrink
let limits = layout::Limits::new(Size::ZERO, limits.max());
let content = self.content.layout(renderer, &limits);
layout::Node::with_children(limits.max(), vec![content])
}

View File

@ -3,6 +3,7 @@ pub mod background_container;
pub mod compound_graphic;
pub mod fill_text;
pub mod image;
pub mod mouse_detector;
pub mod overlay;
pub mod stack;
@ -11,5 +12,6 @@ pub use self::{
background_container::{BackgroundContainer, Padding},
fill_text::FillText,
image::Image,
mouse_detector::MouseDetector,
overlay::Overlay,
};

View File

@ -0,0 +1,112 @@
use iced::{
layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size,
Widget,
};
use std::hash::Hash;
#[derive(Debug, Default)]
pub struct State {
mouse_over: bool,
}
impl State {
pub fn mouse_over(&self) -> bool { self.mouse_over }
}
#[derive(Debug)]
pub struct MouseDetector<'a> {
width: Length,
height: Length,
//on_enter: M,
//on_exit: M,
state: &'a mut State,
}
impl<'a> MouseDetector<'a> {
pub fn new(
state: &'a mut State,
//on_enter: M,
//on_exit: M,
width: Length,
height: Length,
) -> Self {
Self {
state,
//on_enter,
//on_exit,
width,
height,
}
}
}
impl<'a, M, R> Widget<M, R> for MouseDetector<'a>
where
R: self::Renderer,
{
fn width(&self) -> Length { self.width }
fn height(&self) -> Length { self.height }
fn layout(&self, _renderer: &R, limits: &layout::Limits) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
layout::Node::new(limits.resolve(Size::ZERO))
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
_cursor_position: Point,
_messages: &mut Vec<M>,
_renderer: &R,
_clipboard: Option<&dyn Clipboard>,
) {
if let Event::Mouse(mouse::Event::CursorMoved { x, y }) = event {
let bounds = layout.bounds();
let mouse_over = x > bounds.x
&& x < bounds.x + bounds.width
&& y > bounds.y
&& y < bounds.y + bounds.height;
if mouse_over != self.state.mouse_over {
self.state.mouse_over = mouse_over;
/*messages.push(if mouse_over {
self.on_enter.clone()
} else {
self.on_exit.clone()
});*/
}
}
}
fn draw(
&self,
renderer: &mut R,
_defaults: &R::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
) -> R::Output {
renderer.draw(layout.bounds())
}
fn hash_layout(&self, state: &mut Hasher) {
struct Marker;
std::any::TypeId::of::<Marker>().hash(state);
self.width.hash(state);
self.height.hash(state);
}
}
pub trait Renderer: iced::Renderer {
fn draw(&mut self, bounds: Rectangle) -> Self::Output;
}
impl<'a, M, R> From<MouseDetector<'a>> for Element<'a, M, R>
where
R: self::Renderer,
M: 'a,
{
fn from(mouse_detector: MouseDetector<'a>) -> Element<'a, M, R> { Element::new(mouse_detector) }
}

View File

@ -116,14 +116,14 @@ where
let over_size = over.size();
let size = limits.resolve(Size {
width: under_size.width.max(over_size.width),
height: under_size.height.max(over_size.height),
width: under_size.width.max(over_size.width + padding * 2.0),
height: under_size.height.max(over_size.height + padding * 2.0),
});
over.move_to(Point::new(padding, padding));
over.align(self.horizontal_alignment, self.vertical_alignment, size);
layout::Node::with_children(size.pad(padding), vec![over, under])
layout::Node::with_children(size, vec![over, under])
}
fn on_event(
@ -135,9 +135,12 @@ where
renderer: &R,
clipboard: Option<&dyn Clipboard>,
) {
let mut children = layout.children();
let over_layout = children.next().unwrap();
self.over.on_event(
event.clone(),
layout,
over_layout,
cursor_position,
messages,
renderer,
@ -147,16 +150,11 @@ where
// If mouse press check if over the overlay widget before sending to under
// widget
if !matches!(&event, Event::Mouse(mouse::Event::ButtonPressed(_)))
|| !layout
.children()
.next()
.unwrap()
.bounds()
.contains(cursor_position)
|| !over_layout.bounds().contains(cursor_position)
{
self.under.on_event(
event,
layout,
children.next().unwrap(),
cursor_position,
messages,
renderer,