diff --git a/examples/login.rs b/examples/login.rs new file mode 100644 index 000000000..1af86e4dd --- /dev/null +++ b/examples/login.rs @@ -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() +} diff --git a/src/context.rs b/src/context.rs index 820ae1f33..df8794a26 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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 { diff --git a/src/styles/components.rs b/src/styles/components.rs index e3e309f69..af822fdd5 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -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::("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; diff --git a/src/tree.rs b/src/tree.rs index a77ae9797..b6dbd48fb 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -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); @@ -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) { @@ -204,6 +217,24 @@ impl Tree { .hover } + pub fn default_widget(&self) -> Option { + self.data + .lock() + .map_or_else(PoisonError::into_inner, |g| g) + .defaults + .last() + .copied() + } + + pub fn escape_widget(&self) -> Option { + 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; @@ -284,6 +315,8 @@ struct TreeData { active: Option, focus: Option, hover: Option, + defaults: Vec, + escapes: Vec, render_order: Vec, previous_focuses: HashMap, } diff --git a/src/widget.rs b/src/widget.rs index 31f5b1b5c..78ed448c0 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -194,6 +194,34 @@ pub trait MakeWidget: Sized { fn with_next_focus(self, next_focus: impl IntoValue>) -> 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 @@ -265,9 +293,16 @@ where /// An instance of a [`Widget`]. #[derive(Clone, Debug)] pub struct WidgetInstance { + data: Arc, +} + +#[derive(Debug)] +struct WidgetInstanceData { id: WidgetId, - widget: Arc>, + default: bool, + cancel: bool, next_focus: Value>, + widget: Box>, } impl WidgetInstance { @@ -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)), + }), } } @@ -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>, ) -> 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 } @@ -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), ) @@ -333,12 +424,29 @@ impl WidgetInstance { /// This value comes from [`MakeWidget::with_next_focus()`]. #[must_use] pub fn next_focus(&self) -> Option { - 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 for WidgetInstance { fn as_ref(&self) -> &WidgetId { - &self.id + &self.data.id } } @@ -346,7 +454,7 @@ 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) } } @@ -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. diff --git a/src/widgets/align.rs b/src/widgets/align.rs index 22200b94a..ae2373487 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -4,7 +4,7 @@ use kludgine::figures::units::UPx; use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; use crate::context::{AsEventContext, GraphicsContext, LayoutContext}; -use crate::styles::{Edges, FlexibleDimension}; +use crate::styles::{Dimension, Edges, FlexibleDimension}; use crate::value::{IntoValue, Value}; use crate::widget::{MakeWidget, Widget, WidgetRef}; use crate::ConstraintLimit; @@ -32,6 +32,26 @@ impl Align { 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( &mut self, available_space: Size, @@ -102,7 +122,7 @@ impl FrameInfo { fn measure(&self, available: ConstraintLimit, content: UPx) -> (UPx, UPx, UPx) { match available { ConstraintLimit::Known(size) => { - let remaining = size - content; + let remaining = size.saturating_sub(content); let (a, b) = match (self.a, self.b) { (Some(a), Some(b)) => (a, b), (Some(a), None) => (a, remaining - a), diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 2f9b14d63..f8dfa36b9 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -4,7 +4,6 @@ use std::panic::UnwindSafe; use std::time::Duration; use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton}; -use kludgine::app::winit::keyboard::KeyCode; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size}; use kludgine::shapes::Shape; @@ -14,8 +13,11 @@ use kludgine::Color; use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext}; 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::utils::ModifiersExt; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED}; @@ -66,12 +68,15 @@ impl Button { &ButtonActiveBackground, &ButtonBackground, &ButtonHoverBackground, + &PrimaryColor, &Easing, ]); let background_color = if context.active() { styles.get_or_default(&ButtonActiveBackground) } else if context.hovered() { styles.get_or_default(&ButtonHoverBackground) + } else if context.is_default() { + styles.get_or_default(&PrimaryColor) } else { styles.get_or_default(&ButtonBackground) }; @@ -237,13 +242,18 @@ impl Widget for Button { _is_synthetic: bool, context: &mut EventContext<'_, '_>, ) -> 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 { - ElementState::Pressed => context.activate(), - ElementState::Released => { - self.invoke_on_click(); - context.deactivate() + ElementState::Pressed => { + let changed = context.activate(); + if !changed { + // The widget was already active. This is now a repeated keypress + self.invoke_on_click(); + } + changed } + ElementState::Released => context.deactivate(), }; if changed { context.set_needs_redraw(); @@ -271,6 +281,11 @@ impl Widget for Button { } 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); } diff --git a/src/widgets/expand.rs b/src/widgets/expand.rs index 6d265ff8f..efe6d9e6e 100644 --- a/src/widgets/expand.rs +++ b/src/widgets/expand.rs @@ -14,7 +14,13 @@ pub struct Expand { /// The weight to use when splitting available space with multiple /// [`Expand`] widgets. pub weight: u8, - child: WidgetRef, + child: Option, +} + +impl Default for Expand { + fn default() -> Self { + Self::empty() + } } impl Expand { @@ -22,7 +28,16 @@ impl Expand { #[must_use] pub fn new(child: impl MakeWidget) -> 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, } } @@ -34,21 +49,22 @@ impl Expand { #[must_use] pub fn weighted(weight: u8, child: impl MakeWidget) -> Self { Self { - child: WidgetRef::new(child), + child: Some(WidgetRef::new(child)), weight, } } /// Returns a reference to the child widget. #[must_use] - pub fn child(&self) -> &WidgetRef { - &self.child + pub fn child(&self) -> Option<&WidgetRef> { + self.child.as_ref() } } impl Widget for Expand { 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(); } @@ -61,8 +77,15 @@ impl Widget for Expand { ConstraintLimit::Known(available_space.width.max()), ConstraintLimit::Known(available_space.height.max()), ); - let child = self.child.mounted(&mut context.as_event_context()); - let size = context.for_other(&child).layout(available_space); + let child = self + .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( available_space @@ -72,7 +95,11 @@ impl Widget for Expand { .height .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 } } diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 4a95878a8..5bcde2cd2 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -100,6 +100,12 @@ impl Input { } } +impl Default for Input { + fn default() -> Self { + Self::new(String::new()) + } +} + impl Debug for Input { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Input") @@ -393,8 +399,14 @@ impl Widget for Input { ); (false, HANDLED) } - (_, Some(text)) if !context.modifiers().state().primary() && text != "\t" => { - editor.insert_string(&text, None); + (_, Some(text)) + if !context.modifiers().primary() + && text != "\t" // tab + && text != "\r" // enter/return + && text != "\u{1b}" // escape + => + { + editor.insert_string(dbg!(&text), None); (true, HANDLED) } (_, _) => (false, IGNORED), @@ -438,10 +450,12 @@ impl Widget for Input { fn focus(&mut self, context: &mut EventContext<'_, '_>) { context.set_ime_allowed(true); + context.set_needs_redraw(); } fn blur(&mut self, context: &mut EventContext<'_, '_>) { context.set_ime_allowed(false); + context.set_needs_redraw(); } } diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index ec4624319..7971f6307 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -11,7 +11,7 @@ use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContex use crate::styles::Dimension; use crate::value::{Generation, IntoValue, Value}; use crate::widget::{Children, ManagedWidget, Widget, WidgetRef}; -use crate::widgets::{Expand, Resize}; +use crate::widgets::{Expand, Label, Resize}; use crate::ConstraintLimit; /// A widget that displays a collection of [`Widgets`] in a @@ -87,12 +87,21 @@ impl Stack { let guard = widget.lock(); let (mut widget, dimension) = if let Some(expand) = guard.downcast_ref::() { - ( - expand.child().clone(), - StackDimension::Fractional { - weight: expand.weight, - }, - ) + if let Some(child) = expand.child() { + ( + child.clone(), + 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)) = guard.downcast_ref::().and_then(|r| { match self.layout.orientation.orientation { diff --git a/src/window.rs b/src/window.rs index 55dd74407..beaf45dc0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -14,7 +14,7 @@ use kludgine::app::winit::dpi::PhysicalPosition; use kludgine::app::winit::event::{ 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::figures::units::Px; use kludgine::figures::{IntoSigned, Point, Rect, Size}; @@ -30,7 +30,9 @@ use crate::styles::components::VisualOrder; use crate::tree::Tree; use crate::utils::ModifiersExt; 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::{ConstraintLimit, Run}; @@ -243,6 +245,7 @@ struct GooeyWindow { initial_frame: bool, occluded: Dynamic, focused: Dynamic, + keyboard_activated: Option, } impl GooeyWindow @@ -254,6 +257,38 @@ where self.should_close } + + fn keyboard_activate_widget( + &mut self, + is_pressed: bool, + widget: Option, + 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 kludgine::app::WindowBehavior for GooeyWindow @@ -299,6 +334,7 @@ where initial_frame: true, occluded, focused, + keyboard_activated: None, } } @@ -442,32 +478,50 @@ where drop(target); if !handled { - match input.physical_key { - KeyCode::KeyW - if window.modifiers().state().primary() && input.state.is_pressed() => - { - if self.request_close(&mut window) { + match input.logical_key { + Key::Character(ch) if ch == "w" && window.modifiers().primary() => { + if input.state.is_pressed() && self.request_close(&mut window) { window.set_needs_redraw(); } } - KeyCode::Tab - if !window.modifiers().state().possible_shortcut() - && input.state.is_pressed() => - { - let direction = if window.modifiers().state().shift_key() { - VisualOrder::left_to_right().rev() - } 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 mut target = EventContext::new( - WidgetContext::new(target, &self.redraw_status, &mut window), + Key::Tab if !window.modifiers().possible_shortcut() => { + if input.state.is_pressed() { + let direction = if window.modifiers().state().shift_key() { + VisualOrder::left_to_right().rev() + } 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 mut target = EventContext::new( + WidgetContext::new(target, &self.redraw_status, &mut window), + kludgine, + ); + target.advance_focus(direction); + } + } + Key::Enter => { + self.keyboard_activate_widget( + input.state.is_pressed(), + self.root.tree.default_widget(), + &mut window, + kludgine, + ); + } + Key::Escape => { + self.keyboard_activate_widget( + input.state.is_pressed(), + self.root.tree.escape_widget(), + &mut window, kludgine, ); - target.advance_focus(direction); } - _ => {} + _ => { + println!( + "Ignored Keyboard Input: {:?} ({:?}); {:?}", + input.logical_key, input.physical_key, input.state + ); + } } } }