Closes #37
This commit is contained in:
Jonathan Johnson 2023-12-13 16:30:34 -08:00
parent cd4bb5130f
commit aa996a090b
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
4 changed files with 179 additions and 44 deletions

View file

@ -3,29 +3,17 @@ use std::process::exit;
use gooey::value::{Dynamic, Validations}; use gooey::value::{Dynamic, Validations};
use gooey::widget::MakeWidget; use gooey::widget::MakeWidget;
use gooey::widgets::input::{InputValue, MaskedString}; use gooey::widgets::input::{InputValue, MaskedString};
use gooey::widgets::layers::OverlayLayer;
use gooey::widgets::Expand; use gooey::widgets::Expand;
use gooey::Run; use gooey::Run;
use kludgine::figures::units::Lp; use kludgine::figures::units::Lp;
fn main() -> gooey::Result { fn main() -> gooey::Result {
let tooltips = OverlayLayer::default();
let username = Dynamic::default(); let username = Dynamic::default();
let password = Dynamic::default(); let password = Dynamic::default();
let validations = Validations::default(); let validations = Validations::default();
let username_valid = validations.validate(&username, |u: &String| {
if u.is_empty() {
Err("usernames must contain at least one character")
} else {
Ok(())
}
});
let password_valid = validations.validate(&password, |u: &MaskedString| match u.len() {
0..=7 => Err("passwords must be at least 8 characters long"),
_ => Ok(()),
});
// TODO this should be a grid layout to ensure proper visual alignment.
let username_field = "Username" let username_field = "Username"
.align_left() .align_left()
.and( .and(
@ -33,8 +21,18 @@ fn main() -> gooey::Result {
.clone() .clone()
.into_input() .into_input()
.placeholder("Username") .placeholder("Username")
.validation(username_valid) .validation(validations.validate(&username, |u: &String| {
.hint("* required"), if u.is_empty() {
Err("usernames must contain at least one character")
} else {
Ok(())
}
}))
.hint("* required")
.tooltip(
&tooltips,
"If you can't remember your username, that's because this is a demo.",
),
) )
.into_rows(); .into_rows();
@ -45,8 +43,14 @@ fn main() -> gooey::Result {
.clone() .clone()
.into_input() .into_input()
.placeholder("Password") .placeholder("Password")
.validation(password_valid) .validation(
.hint("* required, 8 characters min"), validations.validate(&password, |u: &MaskedString| match u.len() {
0..=7 => Err("passwords must be at least 8 characters long"),
_ => Ok(()),
}),
)
.hint("* required, 8 characters min")
.tooltip(&tooltips, "Passwords are always at least 8 bytes long."),
) )
.into_rows(); .into_rows();
@ -57,6 +61,7 @@ fn main() -> gooey::Result {
exit(0) exit(0)
}) })
.into_escape() .into_escape()
.tooltip(&tooltips, "This button quits the program")
.and(Expand::empty()) .and(Expand::empty())
.and( .and(
"Log In" "Log In"
@ -69,7 +74,7 @@ fn main() -> gooey::Result {
) )
.into_columns(); .into_columns();
username_field let ui = username_field
.and(password_field) .and(password_field)
.and(buttons) .and(buttons)
.into_rows() .into_rows()
@ -77,6 +82,7 @@ fn main() -> gooey::Result {
.width(Lp::inches(3)..Lp::inches(6)) .width(Lp::inches(3)..Lp::inches(6))
.pad() .pad()
.scroll() .scroll()
.centered() .centered();
.run()
ui.and(tooltips).into_layers().run()
} }

View file

@ -39,6 +39,7 @@ use crate::tree::Tree;
use crate::utils::IgnorePoison; use crate::utils::IgnorePoison;
use crate::value::{Dynamic, IntoDynamic, IntoValue, Validation, Value}; use crate::value::{Dynamic, IntoDynamic, IntoValue, Validation, Value};
use crate::widgets::checkbox::{Checkable, CheckboxState}; use crate::widgets::checkbox::{Checkable, CheckboxState};
use crate::widgets::layers::{OverlayLayer, Tooltipped};
use crate::widgets::{ use crate::widgets::{
Align, Button, Checkbox, Collapse, Container, Expand, Layers, Resize, Scroll, Space, Stack, Align, Button, Checkbox, Collapse, Container, Expand, Layers, Resize, Scroll, Space, Stack,
Style, Themed, ThemedMode, Validated, Style, Themed, ThemedMode, Validated,
@ -1077,6 +1078,11 @@ pub trait MakeWidget: Sized {
fn validation(self, validation: impl IntoDynamic<Validation>) -> Validated { fn validation(self, validation: impl IntoDynamic<Validation>) -> Validated {
Validated::new(validation, self) Validated::new(validation, self)
} }
/// Returns a widget that shows `tip` on `layer` when `self` is hovered.
fn tooltip(self, layer: &OverlayLayer, tip: impl MakeWidget) -> Tooltipped {
layer.new_tooltip(tip, self)
}
} }
/// A type that can create a [`WidgetInstance`] with a preallocated /// A type that can create a [`WidgetInstance`] with a preallocated

View file

@ -1,5 +1,7 @@
//! A visual container widget. //! A visual container widget.
use std::ops::Div;
use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::units::{Lp, Px, UPx};
use kludgine::figures::{ use kludgine::figures::{
Abs, Angle, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Zero, Abs, Angle, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Zero,
@ -719,6 +721,18 @@ impl<Unit> ContainerShadow<Unit> {
} }
} }
/// Returns a drop shadow placed `distance` below with a combined
/// blur/spread radius of `blur`.
pub fn drop(distance: Unit, blur: Unit) -> Self
where
Unit: Zero + Div<i32, Output = Unit> + Default + Copy,
{
let half_blur = blur / 2;
Self::new(Point::new(Unit::ZERO, distance))
.blur_radius(half_blur)
.spread(half_blur)
}
/// Sets the shadow color and returns self. /// Sets the shadow color and returns self.
#[must_use] #[must_use]
pub fn color(mut self, color: Color) -> Self { pub fn color(mut self, color: Color) -> Self {

View file

@ -1,21 +1,24 @@
//! Widgets that stack in the Z-direction. //! Widgets that stack in the Z-direction.
use std::fmt; use std::fmt::{self, Debug};
use std::sync::{Arc, Mutex};
use std::time::Duration; use std::time::Duration;
use alot::{LotId, OrderedLots}; use alot::{LotId, OrderedLots};
use gooey::widget::{RootBehavior, WidgetInstance}; use gooey::widget::{RootBehavior, WidgetInstance};
use intentional::Assert; use intentional::Assert;
use kludgine::figures::units::{Px, UPx}; use kludgine::figures::units::{Lp, Px, UPx};
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero};
use crate::animation::easings::EaseOutQuadradic; use crate::animation::easings::EaseOutQuadradic;
use crate::animation::{AnimationTarget, Spawn, ZeroToOne}; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
use crate::utils::IgnorePoison;
use crate::value::{Dynamic, DynamicGuard, Generation, IntoValue, Value}; use crate::value::{Dynamic, DynamicGuard, Generation, IntoValue, Value};
use crate::widget::{ use crate::widget::{
Children, MakeWidget, ManagedWidget, OnceCallback, Widget, WidgetId, WidgetRef, Callback, Children, MakeWidget, ManagedWidget, Widget, WidgetId, WidgetRef, WrapperWidget,
}; };
use crate::widgets::container::ContainerShadow;
use crate::ConstraintLimit; use crate::ConstraintLimit;
/// A Z-direction stack of widgets. /// A Z-direction stack of widgets.
@ -193,6 +196,20 @@ impl OverlayLayer {
}, },
} }
} }
/// Returns a new wudget that shows a `tooltip` when `content` is hovered.
pub fn new_tooltip(&self, tooltip: impl MakeWidget, content: impl MakeWidget) -> Tooltipped {
Tooltipped {
child: WidgetRef::new(content),
data: TooltipData {
target_layer: self.clone(),
tooltip: tooltip.make_widget(),
direction: Direction::Down,
shown_tooltip: Dynamic::default(),
},
show_animation: None,
}
}
} }
impl Widget for OverlayLayer { impl Widget for OverlayLayer {
@ -562,6 +579,7 @@ impl OverlayState {
} }
/// A builder for overlaying a widget on an [`OverlayLayer`]. /// A builder for overlaying a widget on an [`OverlayLayer`].
#[derive(Debug, Clone)]
pub struct OverlayBuilder<'a> { pub struct OverlayBuilder<'a> {
overlay: &'a OverlayLayer, overlay: &'a OverlayLayer,
layout: OverlayLayout, layout: OverlayLayout,
@ -578,40 +596,40 @@ impl OverlayBuilder<'_> {
/// Show this overlay to the left of the specified widget. /// Show this overlay to the left of the specified widget.
#[must_use] #[must_use]
pub fn left_of(mut self, id: WidgetId) -> Self { pub fn left_of(self, id: WidgetId) -> Self {
self.layout.relative_to = Some(id); self.near(id, Direction::Left)
self.layout.direction = Direction::Left;
self
} }
/// Show this overlay to the right of the specified widget. /// Show this overlay to the right of the specified widget.
#[must_use] #[must_use]
pub fn right_of(mut self, id: WidgetId) -> Self { pub fn right_of(self, id: WidgetId) -> Self {
self.layout.relative_to = Some(id); self.near(id, Direction::Right)
self.layout.direction = Direction::Right;
self
} }
/// Show this overlay to show below the specified widget. /// Show this overlay to show below the specified widget.
#[must_use] #[must_use]
pub fn below(mut self, id: WidgetId) -> Self { pub fn below(self, id: WidgetId) -> Self {
self.layout.relative_to = Some(id); self.near(id, Direction::Down)
self.layout.direction = Direction::Down;
self
} }
/// Show this overlay to show above the specified widget. /// Show this overlay to show above the specified widget.
#[must_use] #[must_use]
pub fn above(mut self, id: WidgetId) -> Self { pub fn above(self, id: WidgetId) -> Self {
self.near(id, Direction::Up)
}
/// Shows this overlay near `id` off to the `direction` side.
#[must_use]
pub fn near(mut self, id: WidgetId, direction: Direction) -> Self {
self.layout.relative_to = Some(id); self.layout.relative_to = Some(id);
self.layout.direction = Direction::Up; self.layout.direction = direction;
self self
} }
/// Sets `callback` to be invoked once this overlay is dismissed. /// Sets `callback` to be invoked once this overlay is dismissed.
#[must_use] #[must_use]
pub fn on_dismiss(mut self, callback: OnceCallback) -> Self { pub fn on_dismiss(mut self, callback: Callback) -> Self {
self.layout.on_dismiss = Some(callback); self.layout.on_dismiss = Some(Arc::new(Mutex::new(callback)));
self self
} }
@ -639,7 +657,7 @@ impl OverlayBuilder<'_> {
} }
} }
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Clone)]
struct OverlayLayout { struct OverlayLayout {
widget: WidgetRef, widget: WidgetRef,
opacity: Dynamic<ZeroToOne>, opacity: Dynamic<ZeroToOne>,
@ -647,17 +665,36 @@ struct OverlayLayout {
direction: Direction, direction: Direction,
requires_hover: bool, requires_hover: bool,
layout: Option<Rect<Px>>, layout: Option<Rect<Px>>,
on_dismiss: Option<OnceCallback>, on_dismiss: Option<Arc<Mutex<Callback>>>,
} }
impl Drop for OverlayLayout { impl Drop for OverlayLayout {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(on_dismiss) = self.on_dismiss.take() { if let Some(on_dismiss) = &self.on_dismiss {
let mut on_dismiss = on_dismiss.lock().ignore_poison();
on_dismiss.invoke(()); on_dismiss.invoke(());
} }
} }
} }
impl Eq for OverlayLayout {}
impl PartialEq for OverlayLayout {
fn eq(&self, other: &Self) -> bool {
self.widget == other.widget
&& self.opacity == other.opacity
&& self.relative_to == other.relative_to
&& self.direction == other.direction
&& self.requires_hover == other.requires_hover
&& self.layout == other.layout
&& match (&self.on_dismiss, &other.on_dismiss) {
(Some(this), Some(other)) => Arc::ptr_eq(this, other),
(None, None) => true,
_ => false,
}
}
}
/// A relative direction. /// A relative direction.
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Direction { pub enum Direction {
@ -685,6 +722,7 @@ impl Direction {
} }
/// A handle to an overlay that was shown in an [`OverlayLayer`]. /// A handle to an overlay that was shown in an [`OverlayLayer`].
#[derive(PartialEq, Eq)]
pub struct OverlayHandle { pub struct OverlayHandle {
state: Dynamic<OverlayState>, state: Dynamic<OverlayState>,
id: LotId, id: LotId,
@ -719,3 +757,74 @@ impl Drop for OverlayHandle {
} }
} }
} }
impl Debug for OverlayHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OverlayHandle")
.field("id", &self.id)
.field("dismiss_on_drop", &self.dismiss_on_drop)
.finish_non_exhaustive()
}
}
/// A widget that shows a tooltip when hovered.
#[derive(Debug)]
pub struct Tooltipped {
child: WidgetRef,
show_animation: Option<AnimationHandle>,
data: TooltipData,
}
#[derive(Debug, Clone)]
struct TooltipData {
target_layer: OverlayLayer,
tooltip: WidgetInstance,
direction: Direction,
shown_tooltip: Dynamic<Option<OverlayHandle>>,
}
impl WrapperWidget for Tooltipped {
fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.child
}
fn hover(
&mut self,
_location: Point<Px>,
context: &mut EventContext<'_, '_>,
) -> Option<kludgine::app::winit::window::CursorIcon> {
let background_color = context.theme().surface.highest_container;
let data = self.data.clone();
let my_id = self.child.widget().id();
self.show_animation = Some(
Duration::from_millis(500)
.on_complete(move || {
let mut shown_tooltip = data.shown_tooltip.lock();
if shown_tooltip.is_none() {
*shown_tooltip = Some(
data.target_layer
.build_overlay(
data.tooltip
.clone()
.contain()
.background_color(background_color)
.shadow(ContainerShadow::drop(Lp::mm(1), Lp::mm(2))),
)
.hide_on_unhover()
.near(my_id, data.direction)
.show(),
);
}
})
.spawn(),
);
None
}
fn unhover(&mut self, _context: &mut EventContext<'_, '_>) {
self.show_animation = None;
self.data.shown_tooltip.set(None);
}
}