mirror of
https://github.com/danbulant/cushy
synced 2026-07-05 11:10:34 +00:00
parent
cd4bb5130f
commit
aa996a090b
4 changed files with 179 additions and 44 deletions
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue