mirror of
https://github.com/danbulant/cushy
synced 2026-06-16 04:51:17 +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::widget::MakeWidget;
|
||||
use gooey::widgets::input::{InputValue, MaskedString};
|
||||
use gooey::widgets::layers::OverlayLayer;
|
||||
use gooey::widgets::Expand;
|
||||
use gooey::Run;
|
||||
use kludgine::figures::units::Lp;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let tooltips = OverlayLayer::default();
|
||||
let username = Dynamic::default();
|
||||
let password = Dynamic::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"
|
||||
.align_left()
|
||||
.and(
|
||||
|
|
@ -33,8 +21,18 @@ fn main() -> gooey::Result {
|
|||
.clone()
|
||||
.into_input()
|
||||
.placeholder("Username")
|
||||
.validation(username_valid)
|
||||
.hint("* required"),
|
||||
.validation(validations.validate(&username, |u: &String| {
|
||||
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();
|
||||
|
||||
|
|
@ -45,8 +43,14 @@ fn main() -> gooey::Result {
|
|||
.clone()
|
||||
.into_input()
|
||||
.placeholder("Password")
|
||||
.validation(password_valid)
|
||||
.hint("* required, 8 characters min"),
|
||||
.validation(
|
||||
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();
|
||||
|
||||
|
|
@ -57,6 +61,7 @@ fn main() -> gooey::Result {
|
|||
exit(0)
|
||||
})
|
||||
.into_escape()
|
||||
.tooltip(&tooltips, "This button quits the program")
|
||||
.and(Expand::empty())
|
||||
.and(
|
||||
"Log In"
|
||||
|
|
@ -69,7 +74,7 @@ fn main() -> gooey::Result {
|
|||
)
|
||||
.into_columns();
|
||||
|
||||
username_field
|
||||
let ui = username_field
|
||||
.and(password_field)
|
||||
.and(buttons)
|
||||
.into_rows()
|
||||
|
|
@ -77,6 +82,7 @@ fn main() -> gooey::Result {
|
|||
.width(Lp::inches(3)..Lp::inches(6))
|
||||
.pad()
|
||||
.scroll()
|
||||
.centered()
|
||||
.run()
|
||||
.centered();
|
||||
|
||||
ui.and(tooltips).into_layers().run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ use crate::tree::Tree;
|
|||
use crate::utils::IgnorePoison;
|
||||
use crate::value::{Dynamic, IntoDynamic, IntoValue, Validation, Value};
|
||||
use crate::widgets::checkbox::{Checkable, CheckboxState};
|
||||
use crate::widgets::layers::{OverlayLayer, Tooltipped};
|
||||
use crate::widgets::{
|
||||
Align, Button, Checkbox, Collapse, Container, Expand, Layers, Resize, Scroll, Space, Stack,
|
||||
Style, Themed, ThemedMode, Validated,
|
||||
|
|
@ -1077,6 +1078,11 @@ pub trait MakeWidget: Sized {
|
|||
fn validation(self, validation: impl IntoDynamic<Validation>) -> Validated {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
//! A visual container widget.
|
||||
|
||||
use std::ops::Div;
|
||||
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{
|
||||
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.
|
||||
#[must_use]
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
//! 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 alot::{LotId, OrderedLots};
|
||||
use gooey::widget::{RootBehavior, WidgetInstance};
|
||||
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 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::utils::IgnorePoison;
|
||||
use crate::value::{Dynamic, DynamicGuard, Generation, IntoValue, Value};
|
||||
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;
|
||||
|
||||
/// 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 {
|
||||
|
|
@ -562,6 +579,7 @@ impl OverlayState {
|
|||
}
|
||||
|
||||
/// A builder for overlaying a widget on an [`OverlayLayer`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OverlayBuilder<'a> {
|
||||
overlay: &'a OverlayLayer,
|
||||
layout: OverlayLayout,
|
||||
|
|
@ -578,40 +596,40 @@ impl OverlayBuilder<'_> {
|
|||
|
||||
/// Show this overlay to the left of the specified widget.
|
||||
#[must_use]
|
||||
pub fn left_of(mut self, id: WidgetId) -> Self {
|
||||
self.layout.relative_to = Some(id);
|
||||
self.layout.direction = Direction::Left;
|
||||
self
|
||||
pub fn left_of(self, id: WidgetId) -> Self {
|
||||
self.near(id, Direction::Left)
|
||||
}
|
||||
|
||||
/// Show this overlay to the right of the specified widget.
|
||||
#[must_use]
|
||||
pub fn right_of(mut self, id: WidgetId) -> Self {
|
||||
self.layout.relative_to = Some(id);
|
||||
self.layout.direction = Direction::Right;
|
||||
self
|
||||
pub fn right_of(self, id: WidgetId) -> Self {
|
||||
self.near(id, Direction::Right)
|
||||
}
|
||||
|
||||
/// Show this overlay to show below the specified widget.
|
||||
#[must_use]
|
||||
pub fn below(mut self, id: WidgetId) -> Self {
|
||||
self.layout.relative_to = Some(id);
|
||||
self.layout.direction = Direction::Down;
|
||||
self
|
||||
pub fn below(self, id: WidgetId) -> Self {
|
||||
self.near(id, Direction::Down)
|
||||
}
|
||||
|
||||
/// Show this overlay to show above the specified widget.
|
||||
#[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.direction = Direction::Up;
|
||||
self.layout.direction = direction;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets `callback` to be invoked once this overlay is dismissed.
|
||||
#[must_use]
|
||||
pub fn on_dismiss(mut self, callback: OnceCallback) -> Self {
|
||||
self.layout.on_dismiss = Some(callback);
|
||||
pub fn on_dismiss(mut self, callback: Callback) -> Self {
|
||||
self.layout.on_dismiss = Some(Arc::new(Mutex::new(callback)));
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -639,7 +657,7 @@ impl OverlayBuilder<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct OverlayLayout {
|
||||
widget: WidgetRef,
|
||||
opacity: Dynamic<ZeroToOne>,
|
||||
|
|
@ -647,17 +665,36 @@ struct OverlayLayout {
|
|||
direction: Direction,
|
||||
requires_hover: bool,
|
||||
layout: Option<Rect<Px>>,
|
||||
on_dismiss: Option<OnceCallback>,
|
||||
on_dismiss: Option<Arc<Mutex<Callback>>>,
|
||||
}
|
||||
|
||||
impl Drop for OverlayLayout {
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Direction {
|
||||
|
|
@ -685,6 +722,7 @@ impl Direction {
|
|||
}
|
||||
|
||||
/// A handle to an overlay that was shown in an [`OverlayLayer`].
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct OverlayHandle {
|
||||
state: Dynamic<OverlayState>,
|
||||
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