diff --git a/voxygen/src/menu/char_selection/ui/mod.rs b/voxygen/src/menu/char_selection/ui/mod.rs index 5f750c850f..2b0246efa9 100644 --- a/voxygen/src/menu/char_selection/ui/mod.rs +++ b/voxygen/src/menu/char_selection/ui/mod.rs @@ -5,11 +5,14 @@ use crate::{ self, fonts::IcedFonts as Fonts, ice::{ - component::neat_button, + component::{ + neat_button, + tooltip::{self, WithTooltip}, + }, style, widget::{ mouse_detector, AspectRatioContainer, BackgroundContainer, Image, MouseDetector, - Overlay, Padding, + Overlay, Padding, TooltipManager, }, Element, IcedRenderer, IcedUi as Ui, }, @@ -36,8 +39,11 @@ use vek::Rgba; pub const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0); pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2); +pub const TOOLTIP_BACK_COLOR: Rgba = Rgba::new(20, 18, 10, 255); const FILL_FRAC_ONE: f32 = 0.77; const FILL_FRAC_TWO: f32 = 0.60; +const TOOLTIP_HOVER_DUR: std::time::Duration = std::time::Duration::from_millis(500); +const TOOLTIP_FADE_DUR: std::time::Duration = std::time::Duration::from_millis(500); const STARTER_HAMMER: &str = "common.items.weapons.hammer.starter_hammer"; const STARTER_BOW: &str = "common.items.weapons.bow.starter_bow"; @@ -104,19 +110,12 @@ image_ids_ice! { button: "voxygen.element.buttons.button", button_hover: "voxygen.element.buttons.button_hover", button_press: "voxygen.element.buttons.button_press", - } -} -// TODO: do rotation in widget renderer -/*rotation_image_ids! { - pub struct ImgsRot { - - - // Tooltip Test - tt_side: "voxygen/element/frames/tt_test_edge", + // Tooltips + tt_edge: "voxygen/element/frames/tt_test_edge", tt_corner: "voxygen/element/frames/tt_test_corner_tr", } -}*/ +} pub enum Event { Logout, @@ -235,6 +234,7 @@ struct Controls { // Alpha disclaimer alpha: String, + tooltip_manager: TooltipManager, // Zone for rotating the character with the mouse mouse_detector: mouse_detector::State, // enter: bool, @@ -272,6 +272,7 @@ impl Controls { version, alpha, + tooltip_manager: TooltipManager::new(TOOLTIP_HOVER_DUR, TOOLTIP_FADE_DUR), mouse_detector: Default::default(), mode: Mode::select(), } @@ -279,12 +280,16 @@ impl Controls { fn view(&mut self, settings: &Settings, client: &Client) -> Element { // 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 + // text) + // TODO: if enter key pressed and character is selected then enter the world + + // Maintain tooltip manager + self.tooltip_manager.maintain(); let imgs = &self.imgs; let fonts = &self.fonts; let i18n = &self.i18n; + let tooltip_manager = &self.tooltip_manager; let button_style = style::button::Style::new(imgs.button) .hover_image(imgs.button_hover) @@ -292,6 +297,16 @@ impl Controls { .text_color(TEXT_COLOR) .disabled_text_color(DISABLED_TEXT_COLOR); + let tooltip_style = tooltip::Style { + container: style::container::Style::color_with_image_border( + TOOLTIP_BACK_COLOR, + imgs.tt_corner, + imgs.tt_edge, + ), + text_color: TEXT_COLOR, + text_size: self.fonts.cyri.scale(15), + }; + let version = iced::Text::new(&self.version) .size(self.fonts.cyri.scale(15)) .width(Length::Fill) @@ -382,11 +397,21 @@ impl Controls { Button::new( delete_button, Column::with_children(vec![ - Text::new("Hi").into(), - Text::new("Hi").into(), - Text::new("Hi").into(), + Text::new(&character.character.alias).into(), + // TODO: only construct string once when characters are + // loaded + Text::new( + i18n.get("char_selection.level_fmt").replace( + "{level_nb}", + &character.level.to_string(), + ), + ) + .into(), + Text::new(i18n.get("char_selection.uncanny_valley")) + .into(), ]), ) + .padding(8) .style( style::button::Style::new(imgs.selection) .hover_image(imgs.selection_hover) @@ -394,7 +419,16 @@ impl Controls { ) .width(Length::Fill) .height(Length::Fill) - .on_press(Message::Select(i)), + .on_press(Message::Select(i)) + .with_tooltip( + tooltip_manager, + move || { + tooltip::text( + i18n.get("char_selection.delete_permanently"), + tooltip_style, + ) + }, + ), ) .ratio_of_image(imgs.selection), ) @@ -516,7 +550,7 @@ impl Controls { let over: Element<_> = match info_content { InfoContent::Deletion(_) => Container::new( Column::with_children(vec![ - Text::new(self.i18n.get("char_selection.delete_permanently")) + Text::new(i18n.get("char_selection.delete_permanently")) .size(fonts.cyri.scale(24)) .into(), Row::with_children(vec![ diff --git a/voxygen/src/ui/graphic/mod.rs b/voxygen/src/ui/graphic/mod.rs index 365ec2b7d3..0ff74b738c 100644 --- a/voxygen/src/ui/graphic/mod.rs +++ b/voxygen/src/ui/graphic/mod.rs @@ -28,7 +28,7 @@ pub enum Graphic { Blank, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum Rotation { None, Cw90, diff --git a/voxygen/src/ui/ice/component/mod.rs b/voxygen/src/ui/ice/component/mod.rs index de0a8293eb..7e371a14e6 100644 --- a/voxygen/src/ui/ice/component/mod.rs +++ b/voxygen/src/ui/ice/component/mod.rs @@ -1,2 +1,6 @@ +/// Various composable helpers for making iced ui's pub mod neat_button; +pub mod tooltip; + pub use neat_button::neat_button; +//pub use tooltip::WithTooltip; diff --git a/voxygen/src/ui/ice/component/tooltip.rs b/voxygen/src/ui/ice/component/tooltip.rs new file mode 100644 index 0000000000..be27c83959 --- /dev/null +++ b/voxygen/src/ui/ice/component/tooltip.rs @@ -0,0 +1,40 @@ +use crate::ui::ice as ui; +use iced::{Container, Element, Text}; +use ui::{ + style, + widget::{Tooltip, TooltipManager}, +}; + +// :( all tooltips have to copy because this is needed outside the function +#[derive(Copy, Clone)] +pub struct Style { + pub container: style::container::Style, + pub text_color: iced::Color, + pub text_size: u16, +} + +/// Tooltip that is just text +pub fn text<'a, M: 'a>(text: &'a str, style: Style) -> Element<'a, M, ui::IcedRenderer> { + Container::new( + Text::new(text) + .color(style.text_color) + .size(style.text_size), + ) + .style(style.container) + .into() +} + +pub trait WithTooltip<'a, M, R: iced::Renderer> { + fn with_tooltip(self, manager: &'a TooltipManager, hover_content: H) -> Tooltip<'a, M, R> + where + H: 'a + FnMut() -> Element<'a, M, R>; +} + +impl<'a, M, R: iced::Renderer, E: Into>> WithTooltip<'a, M, R> for E { + fn with_tooltip(self, manager: &'a TooltipManager, hover_content: H) -> Tooltip<'a, M, R> + where + H: 'a + FnMut() -> Element<'a, M, R>, + { + Tooltip::new(self, hover_content, manager) + } +} diff --git a/voxygen/src/ui/ice/renderer/primitive.rs b/voxygen/src/ui/ice/renderer/primitive.rs index 05c08c9969..2ce358bc27 100644 --- a/voxygen/src/ui/ice/renderer/primitive.rs +++ b/voxygen/src/ui/ice/renderer/primitive.rs @@ -1,5 +1,6 @@ use crate::ui::{graphic, ice::widget::image}; +#[derive(Debug)] pub enum Primitive { // Allocation :( Group { diff --git a/voxygen/src/ui/ice/renderer/style/container.rs b/voxygen/src/ui/ice/renderer/style/container.rs index f30ca8cff9..5e446ac996 100644 --- a/voxygen/src/ui/ice/renderer/style/container.rs +++ b/voxygen/src/ui/ice/renderer/style/container.rs @@ -2,6 +2,7 @@ use super::super::super::widget::image; use vek::Rgba; /// Container Border +#[derive(Clone, Copy)] pub enum Border { DoubleCornerless { inner: Rgba, @@ -15,6 +16,7 @@ pub enum Border { } /// Background of the container +#[derive(Clone, Copy)] pub enum Style { Image(image::Handle, Rgba), Color(Rgba, Border), diff --git a/voxygen/src/ui/ice/widget/aspect_ratio_container.rs b/voxygen/src/ui/ice/widget/aspect_ratio_container.rs index e4ad81ea72..c178580e28 100644 --- a/voxygen/src/ui/ice/widget/aspect_ratio_container.rs +++ b/voxygen/src/ui/ice/widget/aspect_ratio_container.rs @@ -160,6 +160,10 @@ where self.content.hash_layout(state); } + + fn overlay(&mut self, layout: Layout<'_>) -> Option> { + self.content.overlay(layout.children().next().unwrap()) + } } pub trait Renderer: iced::Renderer { diff --git a/voxygen/src/ui/ice/widget/background_container.rs b/voxygen/src/ui/ice/widget/background_container.rs index aa8a10583d..1e16e66628 100644 --- a/voxygen/src/ui/ice/widget/background_container.rs +++ b/voxygen/src/ui/ice/widget/background_container.rs @@ -319,6 +319,10 @@ where self.content.hash_layout(state); } + + fn overlay(&mut self, layout: Layout<'_>) -> Option> { + self.content.overlay(layout.children().next().unwrap()) + } } pub trait Renderer: iced::Renderer { diff --git a/voxygen/src/ui/ice/widget/overlay.rs b/voxygen/src/ui/ice/widget/overlay.rs index 7a85d4e8ee..914ba0fc1f 100644 --- a/voxygen/src/ui/ice/widget/overlay.rs +++ b/voxygen/src/ui/ice/widget/overlay.rs @@ -195,6 +195,13 @@ where self.over.hash_layout(state); self.under.hash_layout(state); } + + fn overlay(&mut self, layout: Layout<'_>) -> Option> { + let mut children = layout.children(); + let (over, under) = (&mut self.over, &mut self.under); + over.overlay(children.next().unwrap()) + .or_else(move || under.overlay(children.next().unwrap())) + } } pub trait Renderer: iced::Renderer { diff --git a/voxygen/src/ui/ice/widget/stack.rs b/voxygen/src/ui/ice/widget/stack.rs index dd215dfae1..7fda1b5c2d 100644 --- a/voxygen/src/ui/ice/widget/stack.rs +++ b/voxygen/src/ui/ice/widget/stack.rs @@ -72,6 +72,14 @@ where .iter() .for_each(|child| child.hash_layout(state)); } + + fn overlay(&mut self, layout: Layout<'_>) -> Option> { + self.children + .iter_mut() + .zip(layout.children()) + .filter_map(|(child, layout)| child.overlay(layout)) + .next() + } } pub trait Renderer: iced::Renderer { diff --git a/voxygen/src/ui/ice/widget/tooltip.rs b/voxygen/src/ui/ice/widget/tooltip.rs index d65241259d..e686b2b4a1 100644 --- a/voxygen/src/ui/ice/widget/tooltip.rs +++ b/voxygen/src/ui/ice/widget/tooltip.rs @@ -1,13 +1,14 @@ use iced::{ - layout, mouse, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, - Size, Widget, + layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +}; +use std::{ + hash::Hash, + sync::Mutex, + time::{Duration, Instant}, }; -use std::time::Instant; -use std::time::Duration; -use std::hash::Hash; use vek::*; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] struct Hover { start: Instant, aabr: Aabr, @@ -22,26 +23,29 @@ impl Hover { } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] struct Show { hover_pos: Vec2, aabr: Aabr, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] enum State { Idle, Start(Hover), Showing(Show), - Fading(Instant, Show, Option) + Fading(Instant, Show, Option), } // Reports which widget the mouse is over -pub struct Update((Aabr, Vec2)); +#[derive(Copy, Clone, Debug)] +struct Update((Aabr, Vec2)); +#[derive(Debug)] +// TODO: consider moving all this state into the Renderer pub struct TooltipManager { state: State, - hover_widget: Option>, + update: Mutex>, hover_pos: Vec2, // How long before a tooltip is displayed when hovering hover_dur: Duration, @@ -53,27 +57,35 @@ impl TooltipManager { pub fn new(hover_dur: Duration, fade_dur: Duration) -> Self { Self { state: State::Idle, - hover_widget: None, + update: Mutex::new(None), hover_pos: Default::default(), hover_dur, fade_dur, } } - /// Call this at the top of your view function or for minimum latency at the end of message - /// handling - /// Only call this once per frame as it assumes no updates being received between calls means + + /// Call this at the top of your view function or for minimum latency at the + /// end of message handling /// that there is no tooltipped widget currently being hovered pub fn maintain(&mut self) { + let update = self.update.get_mut().unwrap().take(); // Handle changes based on pointer moving - self.state = if let Some(aabr) = self.hover_widget.take() { + self.state = if let Some(Update((aabr, hover_pos))) = update { + self.hover_pos = hover_pos; match self.state { State::Idle => State::Start(Hover::start(aabr)), State::Start(hover) if hover.aabr != aabr => State::Start(Hover::start(aabr)), State::Start(hover) => State::Start(hover), - State::Showing(show) if show.aabr != aabr => State::Fading(Instant::now(), show, Some(Hover::start(aabr))), + State::Showing(show) if show.aabr != aabr => { + State::Fading(Instant::now(), show, Some(Hover::start(aabr))) + }, State::Showing(show) => State::Showing(show), - State::Fading(start, show, Some(hover)) if hover.aabr == aabr => State::Fading(start, show, Some(hover)), - State::Fading(start, show, _) => State::Fading(start, show, Some(Hover::start(aabr))), + State::Fading(start, show, Some(hover)) if hover.aabr == aabr => { + State::Fading(start, show, Some(hover)) + }, + State::Fading(start, show, _) => { + State::Fading(start, show, Some(Hover::start(aabr))) + }, } } else { match self.state { @@ -85,22 +97,29 @@ impl TooltipManager { // Handle temporal changes self.state = match self.state { - State::Start(Hover { start, aabr}) | State::Fading(_, _, Some(Hover { start, aabr })) if start.elapsed() >= self.hover_dur => - State::Showing(Show { aabr, hover_pos: self.hover_pos }), + State::Start(Hover { start, aabr }) + | State::Fading(_, _, Some(Hover { start, aabr })) + if start.elapsed() >= self.hover_dur => + { + State::Showing(Show { + aabr, + hover_pos: self.hover_pos, + }) + }, State::Fading(start, _, hover) if start.elapsed() >= self.fade_dur => match hover { Some(hover) => State::Start(hover), None => State::Idle, }, - state @ State::Idle | state @ State::Start(_) | state @ State::Showing(_) | state @ State::Fading(_, _, _) => state, - } + state @ State::Idle + | state @ State::Start(_) + | state @ State::Showing(_) + | state @ State::Fading(_, _, _) => state, + }; } - pub fn update(&mut self, update: Update) { - self.hover_widget = Some(update.0.0); - self.hover_pos = update.0.1; - } + fn update(&self, update: Update) { *self.update.lock().unwrap() = Some(update); } - pub fn showing(&self, aabr: Aabr) -> bool { + fn showing(&self, aabr: Aabr) -> bool { match self.state { State::Idle | State::Start(_) => false, State::Showing(show) | State::Fading(_, show, _) => show.aabr == aabr, @@ -108,12 +127,10 @@ impl TooltipManager { } } - /// A widget used to display tooltips when the content element is hovered pub struct Tooltip<'a, M, R: iced::Renderer> { content: Element<'a, M, R>, - hover_content: Option Element<'a, M, R>>>, - on_update: Box M>, + hover_content: Box Element<'a, M, R>>, manager: &'a TooltipManager, } @@ -121,16 +138,14 @@ impl<'a, M, R> Tooltip<'a, M, R> where R: iced::Renderer, { - pub fn new(content: C, hover_content: H, on_update: F, manager: &'a TooltipManager) -> Self + pub fn new(content: C, hover_content: H, manager: &'a TooltipManager) -> Self where C: Into>, - H: 'a + FnOnce() -> Element<'a, M, R>, - F: 'static + Fn(Update) -> M, + H: 'a + FnMut() -> Element<'a, M, R>, { Self { content: content.into(), - hover_content: Some(Box::new(hover_content)), - on_update: Box::new(on_update), + hover_content: Box::new(hover_content), manager, } } @@ -157,13 +172,6 @@ where renderer: &R, clipboard: Option<&dyn Clipboard>, ) { - let bounds = layout.bounds(); - if bounds.contains(cursor_position) { - let aabr = aabr_from_bounds(bounds); - let m_pos = Vec2::new(cursor_position.x.trunc() as i32, cursor_position.y.trunc() as i32); - messages.push((self.on_update)(Update((aabr, m_pos)))); - } - self.content.on_event( event, layout, @@ -181,32 +189,33 @@ where layout: Layout<'_>, cursor_position: Point, ) -> R::Output { - self.content.draw( - renderer, - defaults, - layout, - cursor_position, - ) + let bounds = layout.bounds(); + if bounds.contains(cursor_position) { + let aabr = aabr_from_bounds(bounds); + let m_pos = Vec2::new( + cursor_position.x.trunc() as i32, + cursor_position.y.trunc() as i32, + ); + self.manager.update(Update((aabr, m_pos))); + } + + self.content + .draw(renderer, defaults, layout, cursor_position) } - fn overlay( - &mut self, - layout: Layout<'_>, - ) -> Option> { + fn overlay(&mut self, layout: Layout<'_>) -> Option> { let bounds = layout.bounds(); let aabr = aabr_from_bounds(bounds); - self.manager.showing(aabr) - .then(|| self.hover_content.take()) - .flatten() - .map(|content| iced::overlay::Element::new( - Point { x: self.manager.hover_pos.x as f32, y: self.manager.hover_pos.y as f32 }, - Box::new(Overlay::new( - content(), - bounds, - )) - )) - + self.manager.showing(aabr).then(|| { + iced::overlay::Element::new( + Point { + x: self.manager.hover_pos.x as f32, + y: self.manager.hover_pos.y as f32, + }, + Box::new(Overlay::new((self.hover_content)(), bounds)), + ) + }) } fn hash_layout(&self, state: &mut Hasher) { @@ -236,24 +245,15 @@ struct Overlay<'a, M, R: iced::Renderer> { avoid: Rectangle, } -impl<'a, M, R: iced::Renderer> Overlay<'a, M, R> -{ - pub fn new(content: Element<'a, M, R>, avoid: Rectangle) -> Self { - Self { content, avoid } - } +impl<'a, M, R: iced::Renderer> Overlay<'a, M, R> { + pub fn new(content: Element<'a, M, R>, avoid: Rectangle) -> Self { Self { content, avoid } } } -impl<'a, M, R> iced::Overlay - for Overlay<'a, M, R> +impl<'a, M, R> iced::Overlay for Overlay<'a, M, R> where R: iced::Renderer, { - fn layout( - &self, - renderer: &R, - bounds: Size, - position: Point, - ) -> layout::Node { + fn layout(&self, renderer: &R, bounds: Size, position: Point) -> layout::Node { // TODO: Avoid avoid area let space_below = bounds.height - position.y; let space_above = position.y; @@ -274,8 +274,6 @@ where let mut node = self.content.layout(renderer, &limits); node.move_to(position - iced::Vector::new(0.0, node.size().height)); - - node } fn hash_layout(&self, state: &mut Hasher, position: Point) { @@ -294,6 +292,7 @@ where layout: Layout<'_>, cursor_position: Point, ) -> R::Output { - self.content.draw(renderer, defaults, layout, cursor_position) + self.content + .draw(renderer, defaults, layout, cursor_position) } }