mirror of
https://github.com/danbulant/cushy
synced 2026-07-05 03:00:43 +00:00
Default + Cancel widgets
This commit is contained in:
parent
5e055376e7
commit
bf9836a82b
11 changed files with 450 additions and 60 deletions
70
examples/login.rs
Normal file
70
examples/login.rs
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -805,6 +805,28 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
||||||
self.pending_state.focus.as_ref() == Some(&self.current_node)
|
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.
|
/// Returns the widget this context is for.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn widget(&self) -> &ManagedWidget {
|
pub const fn widget(&self) -> &ManagedWidget {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// A [`Color`] to be used as a highlight color.
|
||||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
pub struct HighlightColor;
|
pub struct HighlightColor;
|
||||||
|
|
|
||||||
33
src/tree.rs
33
src/tree.rs
|
|
@ -32,6 +32,12 @@ impl Tree {
|
||||||
styles: None,
|
styles: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if widget.is_default() {
|
||||||
|
data.defaults.push(id);
|
||||||
|
}
|
||||||
|
if widget.is_escape() {
|
||||||
|
data.escapes.push(id);
|
||||||
|
}
|
||||||
if let Some(parent) = parent {
|
if let Some(parent) = parent {
|
||||||
let parent = data.nodes.get_mut(&parent.id()).expect("missing parent");
|
let parent = data.nodes.get_mut(&parent.id()).expect("missing parent");
|
||||||
parent.children.push(id);
|
parent.children.push(id);
|
||||||
|
|
@ -48,6 +54,13 @@ impl Tree {
|
||||||
pub fn remove_child(&self, child: &ManagedWidget, parent: &ManagedWidget) {
|
pub fn remove_child(&self, child: &ManagedWidget, parent: &ManagedWidget) {
|
||||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||||
data.remove_child(child.id(), parent.id());
|
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>) {
|
pub(crate) fn set_layout(&self, widget: WidgetId, rect: Rect<Px>) {
|
||||||
|
|
@ -204,6 +217,24 @@ impl Tree {
|
||||||
.hover
|
.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 {
|
pub fn is_hovered(&self, id: WidgetId) -> bool {
|
||||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||||
let mut search = data.hover;
|
let mut search = data.hover;
|
||||||
|
|
@ -284,6 +315,8 @@ struct TreeData {
|
||||||
active: Option<WidgetId>,
|
active: Option<WidgetId>,
|
||||||
focus: Option<WidgetId>,
|
focus: Option<WidgetId>,
|
||||||
hover: Option<WidgetId>,
|
hover: Option<WidgetId>,
|
||||||
|
defaults: Vec<WidgetId>,
|
||||||
|
escapes: Vec<WidgetId>,
|
||||||
render_order: Vec<WidgetId>,
|
render_order: Vec<WidgetId>,
|
||||||
previous_focuses: HashMap<WidgetId, WidgetId>,
|
previous_focuses: HashMap<WidgetId, WidgetId>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
src/widget.rs
130
src/widget.rs
|
|
@ -194,6 +194,34 @@ pub trait MakeWidget: Sized {
|
||||||
fn with_next_focus(self, next_focus: impl IntoValue<Option<WidgetId>>) -> WidgetInstance {
|
fn with_next_focus(self, next_focus: impl IntoValue<Option<WidgetId>>) -> WidgetInstance {
|
||||||
self.make_widget().with_next_focus(next_focus)
|
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
|
/// A type that can create a [`WidgetInstance`] with a preallocated
|
||||||
|
|
@ -265,9 +293,16 @@ where
|
||||||
/// An instance of a [`Widget`].
|
/// An instance of a [`Widget`].
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct WidgetInstance {
|
pub struct WidgetInstance {
|
||||||
|
data: Arc<WidgetInstanceData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct WidgetInstanceData {
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
widget: Arc<Mutex<dyn AnyWidget>>,
|
default: bool,
|
||||||
|
cancel: bool,
|
||||||
next_focus: Value<Option<WidgetId>>,
|
next_focus: Value<Option<WidgetId>>,
|
||||||
|
widget: Box<Mutex<dyn AnyWidget>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetInstance {
|
impl WidgetInstance {
|
||||||
|
|
@ -278,9 +313,13 @@ impl WidgetInstance {
|
||||||
W: Widget,
|
W: Widget,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
data: Arc::new(WidgetInstanceData {
|
||||||
widget: Arc::new(Mutex::new(widget)),
|
id: id.into(),
|
||||||
next_focus: Value::default(),
|
next_focus: Value::default(),
|
||||||
|
default: false,
|
||||||
|
cancel: false,
|
||||||
|
widget: Box::new(Mutex::new(widget)),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,19 +334,70 @@ impl WidgetInstance {
|
||||||
/// Returns the unique id of this widget instance.
|
/// Returns the unique id of this widget instance.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn id(&self) -> WidgetId {
|
pub fn id(&self) -> WidgetId {
|
||||||
self.id
|
self.data.id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the widget that should be focused next.
|
/// Sets the widget that should be focused next.
|
||||||
///
|
///
|
||||||
/// Gooey automatically determines reverse tab order by using this same
|
/// Gooey automatically determines reverse tab order by using this same
|
||||||
/// relationship.
|
/// 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]
|
#[must_use]
|
||||||
pub fn with_next_focus(
|
pub fn with_next_focus(
|
||||||
mut self,
|
mut self,
|
||||||
next_focus: impl IntoValue<Option<WidgetId>>,
|
next_focus: impl IntoValue<Option<WidgetId>>,
|
||||||
) -> WidgetInstance {
|
) -> 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,7 +406,8 @@ impl WidgetInstance {
|
||||||
/// occur due to other widget locks being held.
|
/// occur due to other widget locks being held.
|
||||||
pub fn lock(&self) -> WidgetGuard<'_> {
|
pub fn lock(&self) -> WidgetGuard<'_> {
|
||||||
WidgetGuard(
|
WidgetGuard(
|
||||||
self.widget
|
self.data
|
||||||
|
.widget
|
||||||
.lock()
|
.lock()
|
||||||
.map_or_else(PoisonError::into_inner, |g| g),
|
.map_or_else(PoisonError::into_inner, |g| g),
|
||||||
)
|
)
|
||||||
|
|
@ -333,12 +424,29 @@ impl WidgetInstance {
|
||||||
/// This value comes from [`MakeWidget::with_next_focus()`].
|
/// This value comes from [`MakeWidget::with_next_focus()`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn next_focus(&self) -> Option<WidgetId> {
|
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 {
|
impl AsRef<WidgetId> for WidgetInstance {
|
||||||
fn as_ref(&self) -> &WidgetId {
|
fn as_ref(&self) -> &WidgetId {
|
||||||
&self.id
|
&self.data.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -346,7 +454,7 @@ impl Eq for WidgetInstance {}
|
||||||
|
|
||||||
impl PartialEq for WidgetInstance {
|
impl PartialEq for WidgetInstance {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
Arc::ptr_eq(&self.widget, &other.widget)
|
Arc::ptr_eq(&self.data, &other.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,7 +543,7 @@ impl ManagedWidget {
|
||||||
/// Returns the unique id of this widget instance.
|
/// Returns the unique id of this widget instance.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn id(&self) -> WidgetId {
|
pub fn id(&self) -> WidgetId {
|
||||||
self.widget.id
|
self.widget.id()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the next widget to focus after this widget.
|
/// Returns the next widget to focus after this widget.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use kludgine::figures::units::UPx;
|
||||||
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
|
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||||
|
|
||||||
use crate::context::{AsEventContext, GraphicsContext, LayoutContext};
|
use crate::context::{AsEventContext, GraphicsContext, LayoutContext};
|
||||||
use crate::styles::{Edges, FlexibleDimension};
|
use crate::styles::{Dimension, Edges, FlexibleDimension};
|
||||||
use crate::value::{IntoValue, Value};
|
use crate::value::{IntoValue, Value};
|
||||||
use crate::widget::{MakeWidget, Widget, WidgetRef};
|
use crate::widget::{MakeWidget, Widget, WidgetRef};
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
@ -32,6 +32,26 @@ impl Align {
|
||||||
Self::new(FlexibleDimension::Auto, widget)
|
Self::new(FlexibleDimension::Auto, widget)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the left and right edges to 0 and returns self.
|
||||||
|
#[must_use]
|
||||||
|
pub fn fit_horizontally(mut self) -> Self {
|
||||||
|
self.edges.map_mut(|edges| {
|
||||||
|
edges.left = FlexibleDimension::Dimension(Dimension::default());
|
||||||
|
edges.right = FlexibleDimension::Dimension(Dimension::default());
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the top and bottom edges to 0 and returns self.
|
||||||
|
#[must_use]
|
||||||
|
pub fn fit_vertically(mut self) -> Self {
|
||||||
|
self.edges.map_mut(|edges| {
|
||||||
|
edges.top = FlexibleDimension::Dimension(Dimension::default());
|
||||||
|
edges.bottom = FlexibleDimension::Dimension(Dimension::default());
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn measure(
|
fn measure(
|
||||||
&mut self,
|
&mut self,
|
||||||
available_space: Size<ConstraintLimit>,
|
available_space: Size<ConstraintLimit>,
|
||||||
|
|
@ -102,7 +122,7 @@ impl FrameInfo {
|
||||||
fn measure(&self, available: ConstraintLimit, content: UPx) -> (UPx, UPx, UPx) {
|
fn measure(&self, available: ConstraintLimit, content: UPx) -> (UPx, UPx, UPx) {
|
||||||
match available {
|
match available {
|
||||||
ConstraintLimit::Known(size) => {
|
ConstraintLimit::Known(size) => {
|
||||||
let remaining = size - content;
|
let remaining = size.saturating_sub(content);
|
||||||
let (a, b) = match (self.a, self.b) {
|
let (a, b) = match (self.a, self.b) {
|
||||||
(Some(a), Some(b)) => (a, b),
|
(Some(a), Some(b)) => (a, b),
|
||||||
(Some(a), None) => (a, remaining - a),
|
(Some(a), None) => (a, remaining - a),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ use std::panic::UnwindSafe;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton};
|
use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton};
|
||||||
use kludgine::app::winit::keyboard::KeyCode;
|
|
||||||
use kludgine::figures::units::{Px, UPx};
|
use kludgine::figures::units::{Px, UPx};
|
||||||
use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size};
|
use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||||
use kludgine::shapes::Shape;
|
use kludgine::shapes::Shape;
|
||||||
|
|
@ -14,8 +13,11 @@ use kludgine::Color;
|
||||||
use crate::animation::{AnimationHandle, AnimationTarget, Spawn};
|
use crate::animation::{AnimationHandle, AnimationTarget, Spawn};
|
||||||
use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
||||||
use crate::names::Name;
|
use crate::names::Name;
|
||||||
use crate::styles::components::{Easing, HighlightColor, IntrinsicPadding, TextColor};
|
use crate::styles::components::{
|
||||||
|
Easing, HighlightColor, IntrinsicPadding, PrimaryColor, TextColor,
|
||||||
|
};
|
||||||
use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent};
|
use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent};
|
||||||
|
use crate::utils::ModifiersExt;
|
||||||
use crate::value::{Dynamic, IntoValue, Value};
|
use crate::value::{Dynamic, IntoValue, Value};
|
||||||
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
|
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
|
||||||
|
|
||||||
|
|
@ -66,12 +68,15 @@ impl Button {
|
||||||
&ButtonActiveBackground,
|
&ButtonActiveBackground,
|
||||||
&ButtonBackground,
|
&ButtonBackground,
|
||||||
&ButtonHoverBackground,
|
&ButtonHoverBackground,
|
||||||
|
&PrimaryColor,
|
||||||
&Easing,
|
&Easing,
|
||||||
]);
|
]);
|
||||||
let background_color = if context.active() {
|
let background_color = if context.active() {
|
||||||
styles.get_or_default(&ButtonActiveBackground)
|
styles.get_or_default(&ButtonActiveBackground)
|
||||||
} else if context.hovered() {
|
} else if context.hovered() {
|
||||||
styles.get_or_default(&ButtonHoverBackground)
|
styles.get_or_default(&ButtonHoverBackground)
|
||||||
|
} else if context.is_default() {
|
||||||
|
styles.get_or_default(&PrimaryColor)
|
||||||
} else {
|
} else {
|
||||||
styles.get_or_default(&ButtonBackground)
|
styles.get_or_default(&ButtonBackground)
|
||||||
};
|
};
|
||||||
|
|
@ -237,13 +242,18 @@ impl Widget for Button {
|
||||||
_is_synthetic: bool,
|
_is_synthetic: bool,
|
||||||
context: &mut EventContext<'_, '_>,
|
context: &mut EventContext<'_, '_>,
|
||||||
) -> EventHandling {
|
) -> EventHandling {
|
||||||
if input.physical_key == KeyCode::Space {
|
// TODO should this be handled at the window level?
|
||||||
|
if input.text.as_deref() == Some(" ") && !context.modifiers().possible_shortcut() {
|
||||||
let changed = match input.state {
|
let changed = match input.state {
|
||||||
ElementState::Pressed => context.activate(),
|
ElementState::Pressed => {
|
||||||
ElementState::Released => {
|
let changed = context.activate();
|
||||||
self.invoke_on_click();
|
if !changed {
|
||||||
context.deactivate()
|
// The widget was already active. This is now a repeated keypress
|
||||||
|
self.invoke_on_click();
|
||||||
|
}
|
||||||
|
changed
|
||||||
}
|
}
|
||||||
|
ElementState::Released => context.deactivate(),
|
||||||
};
|
};
|
||||||
if changed {
|
if changed {
|
||||||
context.set_needs_redraw();
|
context.set_needs_redraw();
|
||||||
|
|
@ -271,6 +281,11 @@ impl Widget for Button {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn activate(&mut self, context: &mut EventContext<'_, '_>) {
|
fn activate(&mut self, context: &mut EventContext<'_, '_>) {
|
||||||
|
// If we have no buttons pressed, the event should fire on activate not
|
||||||
|
// on deactivate.
|
||||||
|
if self.buttons_pressed == 0 {
|
||||||
|
self.invoke_on_click();
|
||||||
|
}
|
||||||
self.update_background_color(context, true);
|
self.update_background_color(context, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,13 @@ pub struct Expand {
|
||||||
/// The weight to use when splitting available space with multiple
|
/// The weight to use when splitting available space with multiple
|
||||||
/// [`Expand`] widgets.
|
/// [`Expand`] widgets.
|
||||||
pub weight: u8,
|
pub weight: u8,
|
||||||
child: WidgetRef,
|
child: Option<WidgetRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Expand {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Expand {
|
impl Expand {
|
||||||
|
|
@ -22,7 +28,16 @@ impl Expand {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(child: impl MakeWidget) -> Self {
|
pub fn new(child: impl MakeWidget) -> Self {
|
||||||
Self {
|
Self {
|
||||||
child: WidgetRef::new(child),
|
child: Some(WidgetRef::new(child)),
|
||||||
|
weight: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a widget that expands to fill its parent, but has no contents.
|
||||||
|
#[must_use]
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
child: None,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,21 +49,22 @@ impl Expand {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn weighted(weight: u8, child: impl MakeWidget) -> Self {
|
pub fn weighted(weight: u8, child: impl MakeWidget) -> Self {
|
||||||
Self {
|
Self {
|
||||||
child: WidgetRef::new(child),
|
child: Some(WidgetRef::new(child)),
|
||||||
weight,
|
weight,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the child widget.
|
/// Returns a reference to the child widget.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn child(&self) -> &WidgetRef {
|
pub fn child(&self) -> Option<&WidgetRef> {
|
||||||
&self.child
|
self.child.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Expand {
|
impl Widget for Expand {
|
||||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||||
let child = self.child.mounted(&mut context.as_event_context());
|
let Some(child) = &mut self.child else { return };
|
||||||
|
let child = child.mounted(&mut context.as_event_context());
|
||||||
context.for_other(&child).redraw();
|
context.for_other(&child).redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,8 +77,15 @@ impl Widget for Expand {
|
||||||
ConstraintLimit::Known(available_space.width.max()),
|
ConstraintLimit::Known(available_space.width.max()),
|
||||||
ConstraintLimit::Known(available_space.height.max()),
|
ConstraintLimit::Known(available_space.height.max()),
|
||||||
);
|
);
|
||||||
let child = self.child.mounted(&mut context.as_event_context());
|
let child = self
|
||||||
let size = context.for_other(&child).layout(available_space);
|
.child
|
||||||
|
.as_mut()
|
||||||
|
.map(|child| child.mounted(&mut context.as_event_context()));
|
||||||
|
let size = if let Some(child) = &child {
|
||||||
|
context.for_other(child).layout(available_space)
|
||||||
|
} else {
|
||||||
|
Size::default()
|
||||||
|
};
|
||||||
|
|
||||||
let expanded_size = Size::new(
|
let expanded_size = Size::new(
|
||||||
available_space
|
available_space
|
||||||
|
|
@ -72,7 +95,11 @@ impl Widget for Expand {
|
||||||
.height
|
.height
|
||||||
.fit_measured(size.height, context.graphics.scale()),
|
.fit_measured(size.height, context.graphics.scale()),
|
||||||
);
|
);
|
||||||
context.set_child_layout(&child, Rect::from(expanded_size.into_signed()));
|
|
||||||
|
if let Some(child) = child {
|
||||||
|
context.set_child_layout(&child, Rect::from(expanded_size.into_signed()));
|
||||||
|
}
|
||||||
|
|
||||||
expanded_size
|
expanded_size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,12 @@ impl Input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Input {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Debug for Input {
|
impl Debug for Input {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("Input")
|
f.debug_struct("Input")
|
||||||
|
|
@ -393,8 +399,14 @@ impl Widget for Input {
|
||||||
);
|
);
|
||||||
(false, HANDLED)
|
(false, HANDLED)
|
||||||
}
|
}
|
||||||
(_, Some(text)) if !context.modifiers().state().primary() && text != "\t" => {
|
(_, Some(text))
|
||||||
editor.insert_string(&text, None);
|
if !context.modifiers().primary()
|
||||||
|
&& text != "\t" // tab
|
||||||
|
&& text != "\r" // enter/return
|
||||||
|
&& text != "\u{1b}" // escape
|
||||||
|
=>
|
||||||
|
{
|
||||||
|
editor.insert_string(dbg!(&text), None);
|
||||||
(true, HANDLED)
|
(true, HANDLED)
|
||||||
}
|
}
|
||||||
(_, _) => (false, IGNORED),
|
(_, _) => (false, IGNORED),
|
||||||
|
|
@ -438,10 +450,12 @@ impl Widget for Input {
|
||||||
|
|
||||||
fn focus(&mut self, context: &mut EventContext<'_, '_>) {
|
fn focus(&mut self, context: &mut EventContext<'_, '_>) {
|
||||||
context.set_ime_allowed(true);
|
context.set_ime_allowed(true);
|
||||||
|
context.set_needs_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn blur(&mut self, context: &mut EventContext<'_, '_>) {
|
fn blur(&mut self, context: &mut EventContext<'_, '_>) {
|
||||||
context.set_ime_allowed(false);
|
context.set_ime_allowed(false);
|
||||||
|
context.set_needs_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContex
|
||||||
use crate::styles::Dimension;
|
use crate::styles::Dimension;
|
||||||
use crate::value::{Generation, IntoValue, Value};
|
use crate::value::{Generation, IntoValue, Value};
|
||||||
use crate::widget::{Children, ManagedWidget, Widget, WidgetRef};
|
use crate::widget::{Children, ManagedWidget, Widget, WidgetRef};
|
||||||
use crate::widgets::{Expand, Resize};
|
use crate::widgets::{Expand, Label, Resize};
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
/// A widget that displays a collection of [`Widgets`] in a
|
/// A widget that displays a collection of [`Widgets`] in a
|
||||||
|
|
@ -87,12 +87,21 @@ impl Stack {
|
||||||
let guard = widget.lock();
|
let guard = widget.lock();
|
||||||
let (mut widget, dimension) =
|
let (mut widget, dimension) =
|
||||||
if let Some(expand) = guard.downcast_ref::<Expand>() {
|
if let Some(expand) = guard.downcast_ref::<Expand>() {
|
||||||
(
|
if let Some(child) = expand.child() {
|
||||||
expand.child().clone(),
|
(
|
||||||
StackDimension::Fractional {
|
child.clone(),
|
||||||
weight: expand.weight,
|
StackDimension::Fractional {
|
||||||
},
|
weight: expand.weight,
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
WidgetRef::new(Label::new("")), // TODO this should be an empty widget.
|
||||||
|
StackDimension::Fractional {
|
||||||
|
weight: expand.weight,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if let Some((child, size)) =
|
} else if let Some((child, size)) =
|
||||||
guard.downcast_ref::<Resize>().and_then(|r| {
|
guard.downcast_ref::<Resize>().and_then(|r| {
|
||||||
match self.layout.orientation.orientation {
|
match self.layout.orientation.orientation {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use kludgine::app::winit::dpi::PhysicalPosition;
|
||||||
use kludgine::app::winit::event::{
|
use kludgine::app::winit::event::{
|
||||||
DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
||||||
};
|
};
|
||||||
use kludgine::app::winit::keyboard::KeyCode;
|
use kludgine::app::winit::keyboard::Key;
|
||||||
use kludgine::app::WindowBehavior as _;
|
use kludgine::app::WindowBehavior as _;
|
||||||
use kludgine::figures::units::Px;
|
use kludgine::figures::units::Px;
|
||||||
use kludgine::figures::{IntoSigned, Point, Rect, Size};
|
use kludgine::figures::{IntoSigned, Point, Rect, Size};
|
||||||
|
|
@ -30,7 +30,9 @@ use crate::styles::components::VisualOrder;
|
||||||
use crate::tree::Tree;
|
use crate::tree::Tree;
|
||||||
use crate::utils::ModifiersExt;
|
use crate::utils::ModifiersExt;
|
||||||
use crate::value::{Dynamic, IntoDynamic};
|
use crate::value::{Dynamic, IntoDynamic};
|
||||||
use crate::widget::{EventHandling, ManagedWidget, Widget, WidgetInstance, HANDLED, IGNORED};
|
use crate::widget::{
|
||||||
|
EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
|
||||||
|
};
|
||||||
use crate::window::sealed::WindowCommand;
|
use crate::window::sealed::WindowCommand;
|
||||||
use crate::{ConstraintLimit, Run};
|
use crate::{ConstraintLimit, Run};
|
||||||
|
|
||||||
|
|
@ -243,6 +245,7 @@ struct GooeyWindow<T> {
|
||||||
initial_frame: bool,
|
initial_frame: bool,
|
||||||
occluded: Dynamic<bool>,
|
occluded: Dynamic<bool>,
|
||||||
focused: Dynamic<bool>,
|
focused: Dynamic<bool>,
|
||||||
|
keyboard_activated: Option<ManagedWidget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> GooeyWindow<T>
|
impl<T> GooeyWindow<T>
|
||||||
|
|
@ -254,6 +257,38 @@ where
|
||||||
|
|
||||||
self.should_close
|
self.should_close
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_activate_widget(
|
||||||
|
&mut self,
|
||||||
|
is_pressed: bool,
|
||||||
|
widget: Option<WidgetId>,
|
||||||
|
window: &mut RunningWindow<'_>,
|
||||||
|
kludgine: &mut Kludgine,
|
||||||
|
) {
|
||||||
|
if is_pressed {
|
||||||
|
if let Some(default) = widget.and_then(|id| self.root.tree.widget(id)) {
|
||||||
|
if let Some(previously_active) = self.keyboard_activated.take() {
|
||||||
|
EventContext::new(
|
||||||
|
WidgetContext::new(previously_active, &self.redraw_status, window),
|
||||||
|
kludgine,
|
||||||
|
)
|
||||||
|
.deactivate();
|
||||||
|
}
|
||||||
|
EventContext::new(
|
||||||
|
WidgetContext::new(default.clone(), &self.redraw_status, window),
|
||||||
|
kludgine,
|
||||||
|
)
|
||||||
|
.activate();
|
||||||
|
self.keyboard_activated = Some(default);
|
||||||
|
}
|
||||||
|
} else if let Some(keyboard_activated) = self.keyboard_activated.take() {
|
||||||
|
EventContext::new(
|
||||||
|
WidgetContext::new(keyboard_activated, &self.redraw_status, window),
|
||||||
|
kludgine,
|
||||||
|
)
|
||||||
|
.deactivate();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> kludgine::app::WindowBehavior<WindowCommand> for GooeyWindow<T>
|
impl<T> kludgine::app::WindowBehavior<WindowCommand> for GooeyWindow<T>
|
||||||
|
|
@ -299,6 +334,7 @@ where
|
||||||
initial_frame: true,
|
initial_frame: true,
|
||||||
occluded,
|
occluded,
|
||||||
focused,
|
focused,
|
||||||
|
keyboard_activated: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -442,32 +478,50 @@ where
|
||||||
drop(target);
|
drop(target);
|
||||||
|
|
||||||
if !handled {
|
if !handled {
|
||||||
match input.physical_key {
|
match input.logical_key {
|
||||||
KeyCode::KeyW
|
Key::Character(ch) if ch == "w" && window.modifiers().primary() => {
|
||||||
if window.modifiers().state().primary() && input.state.is_pressed() =>
|
if input.state.is_pressed() && self.request_close(&mut window) {
|
||||||
{
|
|
||||||
if self.request_close(&mut window) {
|
|
||||||
window.set_needs_redraw();
|
window.set_needs_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Tab
|
Key::Tab if !window.modifiers().possible_shortcut() => {
|
||||||
if !window.modifiers().state().possible_shortcut()
|
if input.state.is_pressed() {
|
||||||
&& input.state.is_pressed() =>
|
let direction = if window.modifiers().state().shift_key() {
|
||||||
{
|
VisualOrder::left_to_right().rev()
|
||||||
let direction = if window.modifiers().state().shift_key() {
|
} else {
|
||||||
VisualOrder::left_to_right().rev()
|
VisualOrder::left_to_right()
|
||||||
} else {
|
};
|
||||||
VisualOrder::left_to_right()
|
let target = self.root.tree.focused_widget().unwrap_or(self.root.id());
|
||||||
};
|
let target = self.root.tree.widget(target).expect("missing widget");
|
||||||
let target = self.root.tree.focused_widget().unwrap_or(self.root.id());
|
let mut target = EventContext::new(
|
||||||
let target = self.root.tree.widget(target).expect("missing widget");
|
WidgetContext::new(target, &self.redraw_status, &mut window),
|
||||||
let mut target = EventContext::new(
|
kludgine,
|
||||||
WidgetContext::new(target, &self.redraw_status, &mut window),
|
);
|
||||||
|
target.advance_focus(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::Enter => {
|
||||||
|
self.keyboard_activate_widget(
|
||||||
|
input.state.is_pressed(),
|
||||||
|
self.root.tree.default_widget(),
|
||||||
|
&mut window,
|
||||||
kludgine,
|
kludgine,
|
||||||
);
|
);
|
||||||
target.advance_focus(direction);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
Key::Escape => {
|
||||||
|
self.keyboard_activate_widget(
|
||||||
|
input.state.is_pressed(),
|
||||||
|
self.root.tree.escape_widget(),
|
||||||
|
&mut window,
|
||||||
|
kludgine,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!(
|
||||||
|
"Ignored Keyboard Input: {:?} ({:?}); {:?}",
|
||||||
|
input.logical_key, input.physical_key, input.state
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue