Default + Cancel widgets

This commit is contained in:
Jonathan Johnson 2023-11-08 11:03:17 -08:00
parent 5e055376e7
commit bf9836a82b
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
11 changed files with 450 additions and 60 deletions

70
examples/login.rs Normal file
View 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()
}

View file

@ -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 {

View file

@ -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;

View file

@ -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>,
} }

View file

@ -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.

View file

@ -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),

View file

@ -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);
} }

View file

@ -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
} }
} }

View file

@ -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();
} }
} }

View file

@ -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 {

View file

@ -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
);
}
} }
} }
} }