Add a tooltip to the char select screen to test

This commit is contained in:
Imbris 2020-10-18 14:48:01 -04:00
parent 5c85a29111
commit d8854e9300
11 changed files with 203 additions and 100 deletions

View File

@ -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<u8> = 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 {
<VoxelGraphic>
// 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<Message> {
// 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![

View File

@ -28,7 +28,7 @@ pub enum Graphic {
Blank,
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum Rotation {
None,
Cw90,

View File

@ -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;

View File

@ -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<H>(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<Element<'a, M, R>>> WithTooltip<'a, M, R> for E {
fn with_tooltip<H>(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)
}
}

View File

@ -1,5 +1,6 @@
use crate::ui::{graphic, ice::widget::image};
#[derive(Debug)]
pub enum Primitive {
// Allocation :(
Group {

View File

@ -2,6 +2,7 @@ use super::super::super::widget::image;
use vek::Rgba;
/// Container Border
#[derive(Clone, Copy)]
pub enum Border {
DoubleCornerless {
inner: Rgba<u8>,
@ -15,6 +16,7 @@ pub enum Border {
}
/// Background of the container
#[derive(Clone, Copy)]
pub enum Style {
Image(image::Handle, Rgba<u8>),
Color(Rgba<u8>, Border),

View File

@ -160,6 +160,10 @@ where
self.content.hash_layout(state);
}
fn overlay(&mut self, layout: Layout<'_>) -> Option<iced::overlay::Element<'_, M, R>> {
self.content.overlay(layout.children().next().unwrap())
}
}
pub trait Renderer: iced::Renderer {

View File

@ -319,6 +319,10 @@ where
self.content.hash_layout(state);
}
fn overlay(&mut self, layout: Layout<'_>) -> Option<iced::overlay::Element<'_, M, R>> {
self.content.overlay(layout.children().next().unwrap())
}
}
pub trait Renderer: iced::Renderer {

View File

@ -195,6 +195,13 @@ where
self.over.hash_layout(state);
self.under.hash_layout(state);
}
fn overlay(&mut self, layout: Layout<'_>) -> Option<iced::overlay::Element<'_, M, R>> {
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 {

View File

@ -72,6 +72,14 @@ where
.iter()
.for_each(|child| child.hash_layout(state));
}
fn overlay(&mut self, layout: Layout<'_>) -> Option<iced::overlay::Element<'_, M, R>> {
self.children
.iter_mut()
.zip(layout.children())
.filter_map(|(child, layout)| child.overlay(layout))
.next()
}
}
pub trait Renderer: iced::Renderer {

View File

@ -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<i32>,
@ -22,26 +23,29 @@ impl Hover {
}
}
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
struct Show {
hover_pos: Vec2<i32>,
aabr: Aabr<i32>,
}
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
enum State {
Idle,
Start(Hover),
Showing(Show),
Fading(Instant, Show, Option<Hover>)
Fading(Instant, Show, Option<Hover>),
}
// Reports which widget the mouse is over
pub struct Update((Aabr<i32>, Vec2<i32>));
#[derive(Copy, Clone, Debug)]
struct Update((Aabr<i32>, Vec2<i32>));
#[derive(Debug)]
// TODO: consider moving all this state into the Renderer
pub struct TooltipManager {
state: State,
hover_widget: Option<Aabr<i32>>,
update: Mutex<Option<Update>>,
hover_pos: Vec2<i32>,
// 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<i32>) -> bool {
fn showing(&self, aabr: Aabr<i32>) -> 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<Box<dyn 'a + FnOnce() -> Element<'a, M, R>>>,
on_update: Box<dyn Fn(Update) -> M>,
hover_content: Box<dyn 'a + FnMut() -> 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<C, H, F>(content: C, hover_content: H, on_update: F, manager: &'a TooltipManager) -> Self
pub fn new<C, H>(content: C, hover_content: H, manager: &'a TooltipManager) -> Self
where
C: Into<Element<'a, M, R>>,
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<iced::overlay::Element<'_, M, R>> {
fn overlay(&mut self, layout: Layout<'_>) -> Option<iced::overlay::Element<'_, M, R>> {
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<M, R>
for Overlay<'a, M, R>
impl<'a, M, R> iced::Overlay<M, R> 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)
}
}