From 8a2dae3f76b4a4d31c2b7958d931e1d48588729a Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Tue, 7 Nov 2023 08:39:35 -0800 Subject: [PATCH] Next-focus progress gameui no longer needs next_focus for the input to be focused automatically --- examples/gameui.rs | 37 +++++++-------- src/context.rs | 78 ++++++++++++++++++++++++++++++-- src/tree.rs | 35 +++++++++------ src/widget.rs | 110 +++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 213 insertions(+), 47 deletions(-) diff --git a/examples/gameui.rs b/examples/gameui.rs index 5997c30e2..765b33f27 100644 --- a/examples/gameui.rs +++ b/examples/gameui.rs @@ -1,7 +1,7 @@ -use gooey::children; use gooey::value::Dynamic; use gooey::widget::{MakeWidget, HANDLED, IGNORED}; use gooey::widgets::{Canvas, Expand, Input, Label, Scroll, Stack}; +use gooey::{children, Run}; use kludgine::app::winit::event::ElementState; use kludgine::app::winit::keyboard::Key; use kludgine::figures::{Point, Rect}; @@ -12,27 +12,9 @@ fn main() -> gooey::Result { let chat_log = Dynamic::new("Chat log goes here.\n".repeat(100)); let chat_message = Dynamic::new(String::new()); - let input = Input::new(chat_message.clone()) - .on_key({ - let chat_log = chat_log.clone(); - move |input| match (input.state, input.logical_key) { - (ElementState::Pressed, Key::Enter) => { - let new_message = chat_message.map_mut(|text| std::mem::take(text)); - chat_log.map_mut(|chat_log| { - chat_log.push_str(&new_message); - chat_log.push('\n'); - }); - HANDLED - } - _ => IGNORED, - } - }) - .make_widget(); - let input_id = input.id(); - Expand::new(Stack::rows(children![ Expand::new(Stack::columns(children![ - Expand::new(Scroll::vertical(Label::new(chat_log))), + Expand::new(Scroll::vertical(Label::new(chat_log.clone()))), Expand::weighted( 2, Canvas::new(|context| { @@ -46,8 +28,19 @@ fn main() -> gooey::Result { }) ) ])), - input.clone(), + Input::new(chat_message.clone()) + .on_key(move |input| match (input.state, input.logical_key) { + (ElementState::Pressed, Key::Enter) => { + let new_message = chat_message.map_mut(|text| std::mem::take(text)); + chat_log.map_mut(|chat_log| { + chat_log.push_str(&new_message); + chat_log.push('\n'); + }); + HANDLED + } + _ => IGNORED, + }) + .make_widget(), ])) - .with_next_focus(input_id) .run() } diff --git a/src/context.rs b/src/context.rs index 138d82c17..937e71d23 100644 --- a/src/context.rs +++ b/src/context.rs @@ -15,7 +15,7 @@ use crate::graphics::Graphics; use crate::styles::components::HighlightColor; use crate::styles::{ComponentDefaultvalue, ComponentDefinition, Styles}; use crate::value::Dynamic; -use crate::widget::{EventHandling, ManagedWidget, WidgetInstance}; +use crate::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance}; use crate::window::{sealed, RunningWindow}; use crate::ConstraintLimit; @@ -200,8 +200,7 @@ impl<'context, 'window> EventContext<'context, 'window> { } else if let Some(next_focus) = focus.next_focus() { focus = next_focus; } else { - // TODO visually scan the tree for the "next" widget. - break None; + break self.next_focus_after(focus); } }); let new = match self.current_node.tree.focus(focus.as_ref()) { @@ -225,6 +224,79 @@ impl<'context, 'window> EventContext<'context, 'window> { } } } + + fn next_focus_after(&mut self, mut focus: ManagedWidget) -> Option { + // First, look within the current focus for any focusable children. + let stop_at = focus.id(); + if let Some(focus) = self.next_focus_within(&focus, None, stop_at) { + return Some(focus); + } + + // Now, look for the next widget in each hierarchy + let root = loop { + if let Some(focus) = self.next_focus_sibling(&focus, stop_at) { + return Some(focus); + } + let Some(parent) = focus.parent() else { + break focus; + }; + focus = parent; + }; + + // We've exhausted a forward scan, we can now start searching the final + // parent, which is the root. + self.next_focus_within(&root, None, stop_at) + } + + fn next_focus_sibling( + &mut self, + focus: &ManagedWidget, + stop_at: WidgetId, + ) -> Option { + self.next_focus_within(&focus.parent()?, Some(focus.id()), stop_at) + } + + /// Searches for the next focus inside of `focus`, returning `None` if + /// `stop_at` is reached or all children are checked before finding a widget + /// that returns true from `accept_focus`. + fn next_focus_within( + &mut self, + focus: &ManagedWidget, + start_at: Option, + stop_at: WidgetId, + ) -> Option { + let child_layouts = focus.child_layouts(); + // TODO visually sort the layouts + + let mut child_layouts = child_layouts.into_iter().peekable(); + if let Some(start_at) = start_at { + // Skip all children up to `start_at` + while child_layouts.peek()?.0.id() != start_at { + child_layouts.next(); + } + // Skip `start_at` + child_layouts.next(); + } + + for (child, _layout) in child_layouts { + // Ensure we haven't cycled completely. + if stop_at == child.id() { + break; + } + + if child + .lock() + .as_widget() + .accept_focus(&mut self.for_other(child.clone())) + { + return Some(child); + } else if let Some(focus) = self.next_focus_within(&child, None, stop_at) { + return Some(focus); + } + } + + None + } } impl<'context, 'window> Deref for EventContext<'context, 'window> { diff --git a/src/tree.rs b/src/tree.rs index 30445283d..cb8fc9268 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -1,14 +1,12 @@ use std::collections::HashMap; -use std::fmt::Debug; use std::mem; -use std::sync::atomic::{self, AtomicU64}; use std::sync::{Arc, Mutex, PoisonError}; use kludgine::figures::units::Px; use kludgine::figures::{Point, Rect}; use crate::styles::{ComponentDefaultvalue, ComponentDefinition, ComponentType, Styles}; -use crate::widget::{ManagedWidget, WidgetInstance}; +use crate::widget::{ManagedWidget, WidgetId, WidgetInstance}; #[derive(Clone, Default)] pub struct Tree { @@ -37,6 +35,9 @@ impl Tree { let parent = data.nodes.get_mut(&parent.id()).expect("missing parent"); parent.children.push(id); } + if let Some(next_focus) = widget.next_focus() { + data.previous_focuses.insert(next_focus, id); + } ManagedWidget { widget, tree: self.clone(), @@ -85,6 +86,19 @@ impl Tree { } } + pub(crate) fn child_layouts(&self, parent: WidgetId) -> Vec<(ManagedWidget, Rect)> { + let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + data.nodes[&parent] + .children + .iter() + .filter_map(|id| { + data.nodes[id] + .layout + .map(|layout| (data.widget(*id, self).expect("child still owned"), layout)) + }) + .collect() + } + pub(crate) fn hover(&self, new_hover: Option<&ManagedWidget>) -> HoverResults { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); let hovered = new_hover @@ -220,6 +234,7 @@ struct TreeData { focus: Option, hover: Option, render_order: Vec, + previous_focuses: HashMap, } impl TreeData { @@ -242,6 +257,10 @@ impl TreeData { parent.children.remove(index); let mut detached_nodes = removed_node.children; + if let Some(next_focus) = removed_node.widget.next_focus() { + self.previous_focuses.remove(&next_focus); + } + while let Some(node) = detached_nodes.pop() { let mut node = self.nodes.remove(&node).expect("detached node missing"); detached_nodes.append(&mut node.children); @@ -339,13 +358,3 @@ pub struct Node { pub layout: Option>, pub styles: Option, } - -#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)] -pub struct WidgetId(u64); - -impl WidgetId { - pub fn unique() -> Self { - static COUNTER: AtomicU64 = AtomicU64::new(0); - Self(COUNTER.fetch_add(1, atomic::Ordering::Acquire)) - } -} diff --git a/src/widget.rs b/src/widget.rs index 54a1a7fd8..e63325cb2 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -5,6 +5,7 @@ use std::clone::Clone; use std::fmt::Debug; use std::ops::{ControlFlow, Deref}; use std::panic::UnwindSafe; +use std::sync::atomic::{self, AtomicU64}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use kludgine::app::winit::event::{ @@ -15,7 +16,7 @@ use kludgine::figures::{Point, Rect, Size}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::styles::Styles; -use crate::tree::{Tree, WidgetId}; +use crate::tree::Tree; use crate::value::{IntoValue, Value}; use crate::widgets::Style; use crate::window::{RunningWindow, Window, WindowBehavior}; @@ -165,7 +166,7 @@ where } } -/// A type that can create a widget. +/// A type that can create a [`WidgetInstance`]. pub trait MakeWidget: Sized { /// Returns a new widget. fn make_widget(self) -> WidgetInstance; @@ -189,12 +190,28 @@ pub trait MakeWidget: Sized { } } -impl MakeWidget for T +/// A type that can create a [`WidgetInstance`] with a preallocated +/// [`WidgetId`]. +pub trait MakeWidgetWithId: Sized { + /// Returns a new [`WidgetInstance`] whose [`WidgetId`] is `id`. + fn make_with_id(self, id: PendingWidgetId) -> WidgetInstance; +} + +impl MakeWidgetWithId for T where T: Widget, +{ + fn make_with_id(self, id: PendingWidgetId) -> WidgetInstance { + WidgetInstance::with_id(self, id) + } +} + +impl MakeWidget for T +where + T: MakeWidgetWithId, { fn make_widget(self) -> WidgetInstance { - WidgetInstance::new(self) + self.make_with_id(PendingWidgetId::unique()) } } @@ -248,18 +265,27 @@ pub struct WidgetInstance { } impl WidgetInstance { - /// Returns a new instance containing `widget`. - pub fn new(widget: W) -> Self + /// Returns a new instance containing `widget` that is assigned the unique + /// `id` provided. + pub fn with_id(widget: W, id: PendingWidgetId) -> Self where W: Widget, { Self { - id: WidgetId::unique(), + id: id.into(), widget: Arc::new(Mutex::new(widget)), next_focus: Value::default(), } } + /// Returns a new instance containing `widget`. + pub fn new(widget: W) -> Self + where + W: Widget, + { + Self::with_id(widget, PendingWidgetId::unique()) + } + /// Returns the unique id of this widget instance. #[must_use] pub fn id(&self) -> WidgetId { @@ -294,6 +320,15 @@ impl WidgetInstance { pub fn run(self) -> crate::Result { Window::::new(self).run() } + + /// Returns the id of the widget that should receive focus after this + /// widget. + /// + /// This value comes from [`MakeWidget::with_next_focus()`]. + #[must_use] + pub fn next_focus(&self) -> Option { + self.next_focus.get() + } } impl Eq for WidgetInstance {} @@ -399,8 +434,7 @@ impl ManagedWidget { #[must_use] pub fn next_focus(&self) -> Option { self.widget - .next_focus - .get() + .next_focus() .and_then(|next_focus| self.tree.widget(next_focus)) } @@ -449,6 +483,10 @@ impl ManagedWidget { pub(crate) fn reset_child_layouts(&self) { self.tree.reset_child_layouts(self.id()); } + + pub(crate) fn child_layouts(&self) -> Vec<(ManagedWidget, Rect)> { + self.tree.child_layouts(self.id()) + } } impl PartialEq for ManagedWidget { @@ -592,3 +630,57 @@ impl WidgetRef { widget.clone() } } + +/// The unique id of a [`WidgetInstance`]. +/// +/// Each [`WidgetInstance`] is guaranteed to have a unique [`WidgetId`] across +/// the lifetime of an application. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)] +pub struct WidgetId(u64); + +impl WidgetId { + fn unique() -> Self { + static COUNTER: AtomicU64 = AtomicU64::new(0); + Self(COUNTER.fetch_add(1, atomic::Ordering::Acquire)) + } +} + +/// A [`WidgetId`] that has not been assigned to a [`WidgetInstance`]. +/// +/// This type is passed to [`MakeWidgetWithId::make_with_id()`] to create a +/// [`WidgetInstance`] with a preallocated id. +/// +/// This type cannot be cloned or copied to ensure only a single widget can be +/// assigned a given [`WidgetId`]. The contained [`WidgetId`] can be accessed +/// via [`id()`](Self::id), `Into`, or `Deref`. +#[derive(Eq, PartialEq, Debug)] +pub struct PendingWidgetId(WidgetId); + +impl PendingWidgetId { + /// Returns a newly allocated [`WidgetId`] that is guaranteed to be unique + /// for the lifetime of the application. + #[must_use] + pub fn unique() -> Self { + Self(WidgetId::unique()) + } + + /// Returns the contained widget id. + #[must_use] + pub const fn id(&self) -> WidgetId { + self.0 + } +} + +impl From for WidgetId { + fn from(value: PendingWidgetId) -> Self { + value.0 + } +} + +impl Deref for PendingWidgetId { + type Target = WidgetId; + + fn deref(&self) -> &Self::Target { + &self.0 + } +}