diff --git a/src/styles.rs b/src/styles.rs index 721268c..573e927 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use crate::animation::{EasingFunction, ZeroToOne}; use crate::names::Name; +use crate::styles::components::{FocusableWidgets, VisualOrder}; use crate::utils::Lazy; use crate::value::{IntoValue, Value}; @@ -151,6 +152,10 @@ pub enum Component { Custom(CustomComponent), /// An easing function for animations. Easing(EasingFunction), + /// A visual ordering to use for layout. + VisualOrder(VisualOrder), + /// A description of what widgets should be focusable. + FocusableWidgets(FocusableWidgets), } impl From for Component { diff --git a/src/styles/components.rs b/src/styles/components.rs index af822fd..69f8fb5 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -7,7 +7,9 @@ use kludgine::Color; use crate::animation::easings::{EaseInQuadradic, EaseOutQuadradic}; use crate::animation::EasingFunction; -use crate::styles::{ComponentDefinition, ComponentName, Dimension, Global, NamedComponent}; +use crate::styles::{ + Component, ComponentDefinition, ComponentName, Dimension, Global, NamedComponent, +}; /// The [`Dimension`] to use as the size to render text. #[derive(Clone, Copy, Eq, PartialEq, Debug)] @@ -181,7 +183,7 @@ impl ComponentDefinition for EasingOut { } /// A 2d ordering configuration. -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Debug, Clone, Eq, PartialEq)] pub struct VisualOrder { /// The ordering to apply horizontally. pub horizontal: HorizontalOrder, @@ -218,14 +220,43 @@ impl VisualOrder { } } -impl NamedComponent for VisualOrder { +/// The [`VisualOrder`] strategy to use when laying out content. +#[derive(Debug)] +pub struct LayoutOrder; + +impl NamedComponent for LayoutOrder { fn name(&self) -> Cow<'_, ComponentName> { Cow::Owned(ComponentName::named::("visual_order")) } } +impl ComponentDefinition for LayoutOrder { + type ComponentType = VisualOrder; + + fn default_value(&self) -> Self::ComponentType { + VisualOrder::left_to_right() + } +} + +impl From for Component { + fn from(value: VisualOrder) -> Self { + Self::VisualOrder(value) + } +} + +impl TryFrom for VisualOrder { + type Error = Component; + + fn try_from(value: Component) -> Result { + match value { + Component::VisualOrder(order) => Ok(order), + other => Err(other), + } + } +} + /// A horizontal direction. -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum HorizontalOrder { /// Describes an order starting at the left and proceeding to the right. LeftToRight, @@ -252,7 +283,7 @@ impl HorizontalOrder { } /// A vertical direction. -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum VerticalOrder { /// Describes an order starting at the top and proceeding to the bottom. TopToBottom, @@ -284,3 +315,63 @@ impl VerticalOrder { } } } + +/// The set of controls to allow focusing via tab key and initial focus +/// selection. +pub struct AutoFocusableControls; + +impl NamedComponent for AutoFocusableControls { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("focus")) + } +} + +impl ComponentDefinition for AutoFocusableControls { + type ComponentType = FocusableWidgets; + + fn default_value(&self) -> Self::ComponentType { + FocusableWidgets::default() + } +} + +impl From for Component { + fn from(value: FocusableWidgets) -> Self { + Self::FocusableWidgets(value) + } +} + +impl TryFrom for FocusableWidgets { + type Error = Component; + + fn try_from(value: Component) -> Result { + match value { + Component::FocusableWidgets(focus) => Ok(focus), + other => Err(other), + } + } +} + +/// A configuration option to control which controls should be able to receive +/// focus through keyboard focus handling or initial focus handling. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] +pub enum FocusableWidgets { + /// Allow all widgets that can respond to keyboard input to accept focus. + #[default] + All, + /// Only allow widgets that expect textual input to accept focus. + OnlyTextual, +} + +impl FocusableWidgets { + /// Returns true if all controls should be focusable. + #[must_use] + pub const fn is_all(self) -> bool { + matches!(self, Self::All) + } + + /// Returns true if only textual should be focusable. + #[must_use] + pub const fn is_only_textual(self) -> bool { + matches!(self, Self::OnlyTextual) + } +} diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 8ab76ad..a40c2ee 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -14,7 +14,7 @@ use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::names::Name; use crate::styles::components::{ - Easing, HighlightColor, IntrinsicPadding, PrimaryColor, TextColor, + AutoFocusableControls, Easing, HighlightColor, IntrinsicPadding, PrimaryColor, TextColor, }; use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; use crate::utils::ModifiersExt; @@ -180,9 +180,8 @@ impl Widget for Button { true } - fn accept_focus(&mut self, _context: &mut EventContext<'_, '_>) -> bool { - // TODO this should be driven by a "focus_all_widgets" setting that hopefully can be queried from the OS. - self.enabled.get() + fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { + self.enabled.get() && context.query_style(&AutoFocusableControls).is_all() } fn mouse_down( diff --git a/src/window.rs b/src/window.rs index 8cd1cc1..4e06c84 100644 --- a/src/window.rs +++ b/src/window.rs @@ -27,7 +27,7 @@ use crate::context::{ WidgetContext, }; use crate::graphics::Graphics; -use crate::styles::components::VisualOrder; +use crate::styles::components::LayoutOrder; use crate::tree::Tree; use crate::utils::ModifiersExt; use crate::value::{Dynamic, IntoDynamic}; @@ -492,18 +492,19 @@ where } 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 reverse = window.modifiers().state().shift_key(); + 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); + let mut visual_order = target.query_style(&LayoutOrder); + if reverse { + visual_order = visual_order.rev(); + } + target.advance_focus(visual_order); } } Key::Enter => {