Skip to content

Commit

Permalink
Default + Cancel widgets
Browse files Browse the repository at this point in the history
  • Loading branch information
ecton committed Nov 8, 2023
1 parent 5e05537 commit bf9836a
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 60 deletions.
70 changes: 70 additions & 0 deletions examples/login.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use std::process::exit;

use gooey::value::Dynamic;
use gooey::widget::MakeWidget;
use gooey::widgets::{Align, Button, Expand, Input, Label, Resize, Stack};
use gooey::{children, Run, WithClone};
use kludgine::figures::units::Lp;

fn main() -> gooey::Result {
let username = Dynamic::default();
let password = Dynamic::default();

// TODO This is absolutely horrible. The problem is that within for_each,
// the value is still locked. Thus, we can't have a generic callback that
// tries to lock the value that is being mapped in for_each.
//
// We might be able to make a genericized implementation for_each for
// tuples, ie, (&Dynamic, &Dynamic).for_each(|(a, b)| ..).
let valid = Dynamic::default();
username.for_each((&valid, &password).with_clone(|(valid, password)| {
move |username: &String| {
password.map_ref(|password| valid.update(validate(username, password)))
}
}));
password.for_each((&valid, &username).with_clone(|(valid, username)| {
move |password: &String| {
username.map_ref(|username| valid.update(validate(username, password)))
}
}));

Expand::new(Align::centered(Resize::width(
// TODO We need a min/max range for the Resize widget
Lp::points(400),
Stack::rows(children![
Stack::columns(children![
Label::new("Username"),
Expand::new(Align::centered(Input::new(username.clone())).fit_horizontally()),
]),
Stack::columns(children![
Label::new("Password"),
Expand::new(
Align::centered(
// TODO secure input
Input::new(password.clone())
)
.fit_horizontally()
),
]),
Stack::columns(children![
Button::new("Cancel").on_click(|_| exit(0)).into_escape(),
Expand::empty(),
Button::new("Log In")
.on_click(move |_| {
if valid.get() {
println!("Welcome, {}", username.get());
exit(0);
} else {
eprintln!("Enter a username and password")
}
})
.into_default(), // TODO enable/disable based on valid
]),
]),
)))
.run()
}

fn validate(username: &String, password: &String) -> bool {
!username.is_empty() && !password.is_empty()
}
22 changes: 22 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,28 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
self.pending_state.focus.as_ref() == Some(&self.current_node)
}

/// Returns true if this widget is the target to activate when the user
/// triggers a default action.
///
/// See
/// [`MakeWidget::into_default()`](crate::widget::MakeWidget::into_default)
/// for more information.
#[must_use]
pub fn is_default(&self) -> bool {
self.current_node.tree.default_widget() == Some(self.current_node.id())
}

/// Returns true if this widget is the target to activate when the user
/// triggers an escape action.
///
/// See
/// [`MakeWidget::into_escape()`](crate::widget::MakeWidget::into_escape)
/// for more information.
#[must_use]
pub fn is_escape(&self) -> bool {
self.current_node.tree.escape_widget() == Some(self.current_node.id())
}

/// Returns the widget this context is for.
#[must_use]
pub const fn widget(&self) -> &ManagedWidget {
Expand Down
18 changes: 18 additions & 0 deletions src/styles/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ impl ComponentDefinition for TextColor {
}
}

/// A [`Color`] to be used as a highlight color.
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct PrimaryColor;

impl NamedComponent for PrimaryColor {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("primary_color"))
}
}

impl ComponentDefinition for PrimaryColor {
type ComponentType = Color;

fn default_value(&self) -> Color {
Color::BLUE
}
}

/// A [`Color`] to be used as a highlight color.
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct HighlightColor;
Expand Down
33 changes: 33 additions & 0 deletions src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ impl Tree {
styles: None,
},
);
if widget.is_default() {
data.defaults.push(id);
}
if widget.is_escape() {
data.escapes.push(id);
}
if let Some(parent) = parent {
let parent = data.nodes.get_mut(&parent.id()).expect("missing parent");
parent.children.push(id);
Expand All @@ -48,6 +54,13 @@ impl Tree {
pub fn remove_child(&self, child: &ManagedWidget, parent: &ManagedWidget) {
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
data.remove_child(child.id(), parent.id());

if child.widget.is_default() {
data.defaults.retain(|id| *id != child.id());
}
if child.widget.is_escape() {
data.escapes.retain(|id| *id != child.id());
}
}

pub(crate) fn set_layout(&self, widget: WidgetId, rect: Rect<Px>) {
Expand Down Expand Up @@ -204,6 +217,24 @@ impl Tree {
.hover
}

pub fn default_widget(&self) -> Option<WidgetId> {
self.data
.lock()
.map_or_else(PoisonError::into_inner, |g| g)
.defaults
.last()
.copied()
}

pub fn escape_widget(&self) -> Option<WidgetId> {
self.data
.lock()
.map_or_else(PoisonError::into_inner, |g| g)
.escapes
.last()
.copied()
}

pub fn is_hovered(&self, id: WidgetId) -> bool {
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
let mut search = data.hover;
Expand Down Expand Up @@ -284,6 +315,8 @@ struct TreeData {
active: Option<WidgetId>,
focus: Option<WidgetId>,
hover: Option<WidgetId>,
defaults: Vec<WidgetId>,
escapes: Vec<WidgetId>,
render_order: Vec<WidgetId>,
previous_focuses: HashMap<WidgetId, WidgetId>,
}
Expand Down
130 changes: 119 additions & 11 deletions src/widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,34 @@ pub trait MakeWidget: Sized {
fn with_next_focus(self, next_focus: impl IntoValue<Option<WidgetId>>) -> WidgetInstance {
self.make_widget().with_next_focus(next_focus)
}

/// Sets this widget as a "default" widget.
///
/// Default widgets are automatically activated when the user signals they
/// are ready for the default action to occur.
///
/// Example widgets this is used for are:
///
/// - Submit buttons on forms
/// - Ok buttons
#[must_use]
fn into_default(self) -> WidgetInstance {
self.make_widget().into_default()
}

/// Sets this widget as an "escape" widget.
///
/// Escape widgets are automatically activated when the user signals they
/// are ready to escape their current situation.
///
/// Example widgets this is used for are:
///
/// - Close buttons
/// - Cancel buttons
#[must_use]
fn into_escape(self) -> WidgetInstance {
self.make_widget().into_escape()
}
}

/// A type that can create a [`WidgetInstance`] with a preallocated
Expand Down Expand Up @@ -265,9 +293,16 @@ where
/// An instance of a [`Widget`].
#[derive(Clone, Debug)]
pub struct WidgetInstance {
data: Arc<WidgetInstanceData>,
}

#[derive(Debug)]
struct WidgetInstanceData {
id: WidgetId,
widget: Arc<Mutex<dyn AnyWidget>>,
default: bool,
cancel: bool,
next_focus: Value<Option<WidgetId>>,
widget: Box<Mutex<dyn AnyWidget>>,
}

impl WidgetInstance {
Expand All @@ -278,9 +313,13 @@ impl WidgetInstance {
W: Widget,
{
Self {
id: id.into(),
widget: Arc::new(Mutex::new(widget)),
next_focus: Value::default(),
data: Arc::new(WidgetInstanceData {
id: id.into(),
next_focus: Value::default(),
default: false,
cancel: false,
widget: Box::new(Mutex::new(widget)),
}),
}
}

Expand All @@ -295,19 +334,70 @@ impl WidgetInstance {
/// Returns the unique id of this widget instance.
#[must_use]
pub fn id(&self) -> WidgetId {
self.id
self.data.id
}

/// Sets the widget that should be focused next.
///
/// Gooey automatically determines reverse tab order by using this same
/// relationship.
///
/// # Panics
///
/// This function can only be called when one instance of the widget exists.
/// If any clones exist, a panic will occur.
#[must_use]
pub fn with_next_focus(
mut self,
next_focus: impl IntoValue<Option<WidgetId>>,
) -> WidgetInstance {
self.next_focus = next_focus.into_value();
let data = Arc::get_mut(&mut self.data)
.expect("with_next_focus can only be called on newly created widget instances");
data.next_focus = next_focus.into_value();
self
}

/// Sets this widget as a "default" widget.
///
/// Default widgets are automatically activated when the user signals they
/// are ready for the default action to occur.
///
/// Example widgets this is used for are:
///
/// - Submit buttons on forms
/// - Ok buttons
///
/// # Panics
///
/// This function can only be called when one instance of the widget exists.
/// If any clones exist, a panic will occur.
#[must_use]
pub fn into_default(mut self) -> WidgetInstance {
let data = Arc::get_mut(&mut self.data)
.expect("with_next_focus can only be called on newly created widget instances");
data.default = true;
self
}

/// Sets this widget as an "escape" widget.
///
/// Escape widgets are automatically activated when the user signals they
/// are ready to escape their current situation.
///
/// Example widgets this is used for are:
///
/// - Close buttons
/// - Cancel buttons
///
/// # Panics
///
/// This function can only be called when one instance of the widget exists.
/// If any clones exist, a panic will occur.
#[must_use]
pub fn into_escape(mut self) -> WidgetInstance {
let data = Arc::get_mut(&mut self.data)
.expect("with_next_focus can only be called on newly created widget instances");
data.cancel = true;
self
}

Expand All @@ -316,7 +406,8 @@ impl WidgetInstance {
/// occur due to other widget locks being held.
pub fn lock(&self) -> WidgetGuard<'_> {
WidgetGuard(
self.widget
self.data
.widget
.lock()
.map_or_else(PoisonError::into_inner, |g| g),
)
Expand All @@ -333,20 +424,37 @@ impl WidgetInstance {
/// This value comes from [`MakeWidget::with_next_focus()`].
#[must_use]
pub fn next_focus(&self) -> Option<WidgetId> {
self.next_focus.get()
self.data.next_focus.get()
}

/// Returns true if this is a default widget.
///
/// See [`MakeWidget::into_default()`] for more information.
#[must_use]
pub fn is_default(&self) -> bool {
self.data.default
}

/// Returns true if this is an escape widget.
///
/// See [`MakeWidget::into_escape()`] for more information.
#[must_use]
pub fn is_escape(&self) -> bool {
self.data.cancel
}
}

impl AsRef<WidgetId> for WidgetInstance {
fn as_ref(&self) -> &WidgetId {
&self.id
&self.data.id
}
}

impl Eq for WidgetInstance {}

impl PartialEq for WidgetInstance {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.widget, &other.widget)
Arc::ptr_eq(&self.data, &other.data)
}
}

Expand Down Expand Up @@ -435,7 +543,7 @@ impl ManagedWidget {
/// Returns the unique id of this widget instance.
#[must_use]
pub fn id(&self) -> WidgetId {
self.widget.id
self.widget.id()
}

/// Returns the next widget to focus after this widget.
Expand Down
Loading

0 comments on commit bf9836a

Please sign in to comment.