diff --git a/examples/gameui.rs b/examples/gameui.rs index c112a12..5997c30 100644 --- a/examples/gameui.rs +++ b/examples/gameui.rs @@ -28,6 +28,7 @@ fn main() -> gooey::Result { } }) .make_widget(); + let input_id = input.id(); Expand::new(Stack::rows(children![ Expand::new(Stack::columns(children![ @@ -47,6 +48,6 @@ fn main() -> gooey::Result { ])), input.clone(), ])) - .with_next_focus(input) + .with_next_focus(input_id) .run() } diff --git a/src/context.rs b/src/context.rs index f90d196..138d82c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -166,7 +166,7 @@ impl<'context, 'window> EventContext<'context, 'window> { pub(crate) fn apply_pending_state(&mut self) { let active = self.pending_state.active.clone(); - if self.current_node.tree.active_widget() != active.as_ref().map(|active| active.id) { + if self.current_node.tree.active_widget() != active.as_ref().map(ManagedWidget::id) { let new = match self.current_node.tree.activate(active.as_ref()) { Ok(old) => { if let Some(old) = old { @@ -178,17 +178,32 @@ impl<'context, 'window> EventContext<'context, 'window> { Err(_) => false, }; if new { - if let Some(active) = active { + if let Some(active) = &active { active .lock() .as_widget() .activate(&mut self.for_other(active.clone())); } + self.pending_state.active = active; } } let focus = self.pending_state.focus.clone(); - if self.current_node.tree.focused_widget() != focus.as_ref().map(|focus| focus.id) { + if self.current_node.tree.focused_widget() != focus.as_ref().map(ManagedWidget::id) { + let focus = focus.and_then(|mut focus| loop { + if focus + .lock() + .as_widget() + .accept_focus(&mut self.for_other(focus.clone())) + { + break Some(focus); + } else if let Some(next_focus) = focus.next_focus() { + focus = next_focus; + } else { + // TODO visually scan the tree for the "next" widget. + break None; + } + }); let new = match self.current_node.tree.focus(focus.as_ref()) { Ok(old) => { if let Some(old) = old { @@ -200,12 +215,13 @@ impl<'context, 'window> EventContext<'context, 'window> { Err(_) => false, }; if new { - if let Some(focus) = focus { + if let Some(focus) = &focus { focus .lock() .as_widget() .focus(&mut self.for_other(focus.clone())); } + self.pending_state.focus = focus; } } } @@ -324,6 +340,14 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' } } +impl Drop for GraphicsContext<'_, '_, '_, '_, '_> { + fn drop(&mut self) { + if matches!(self.widget.pending_state, PendingState::Owned(_)) { + self.as_event_context().apply_pending_state(); + } + } +} + impl<'context, 'window, 'clip, 'gfx, 'pass> Deref for GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> { @@ -497,11 +521,11 @@ impl<'context, 'window> WidgetContext<'context, 'window> { focus: current_node .tree .focused_widget() - .map(|id| current_node.tree.widget(id)), + .and_then(|id| current_node.tree.widget(id)), active: current_node .tree .active_widget() - .map(|id| current_node.tree.widget(id)), + .and_then(|id| current_node.tree.widget(id)), }), current_node, redraw_status, diff --git a/src/tree.rs b/src/tree.rs index aabe7b9..3044528 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -1,8 +1,9 @@ +use std::collections::HashMap; use std::fmt::Debug; use std::mem; +use std::sync::atomic::{self, AtomicU64}; use std::sync::{Arc, Mutex, PoisonError}; -use alot::{LotId, Lots}; use kludgine::figures::units::Px; use kludgine::figures::{Point, Rect}; @@ -21,19 +22,22 @@ impl Tree { parent: Option<&ManagedWidget>, ) -> ManagedWidget { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); - let id = WidgetId(data.nodes.push(Node { - widget: widget.clone(), - children: Vec::new(), - parent: parent.map(|parent| parent.id), - layout: None, - styles: None, - })); + let id = widget.id(); + data.nodes.insert( + id, + Node { + widget: widget.clone(), + children: Vec::new(), + parent: parent.map(ManagedWidget::id), + layout: None, + styles: None, + }, + ); if let Some(parent) = parent { - let parent = &mut data.nodes[parent.id.0]; + let parent = data.nodes.get_mut(&parent.id()).expect("missing parent"); parent.children.push(id); } ManagedWidget { - id, widget, tree: self.clone(), } @@ -41,26 +45,31 @@ 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); + data.remove_child(child.id(), parent.id()); } pub(crate) fn set_layout(&self, widget: WidgetId, rect: Rect) { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); - rect.extents(); - data.nodes[widget.0].layout = Some(rect); + data.render_order.push(widget); - let mut children_to_offset = data.nodes[widget.0].children.clone(); + let node = data.nodes.get_mut(&widget).expect("missing widget"); + node.layout = Some(rect); + let mut children_to_offset = node.children.clone(); while let Some(child) = children_to_offset.pop() { - if let Some(layout) = &mut data.nodes[child.0].layout { + if let Some(layout) = data + .nodes + .get_mut(&child) + .and_then(|child| child.layout.as_mut()) + { layout.origin += rect.origin; - children_to_offset.extend(data.nodes[child.0].children.iter().copied()); + children_to_offset.extend(data.nodes[&child].children.iter().copied()); } } } pub(crate) fn layout(&self, widget: WidgetId) -> Option> { let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); - data.nodes[widget.0].layout + data.nodes[&widget].layout } pub(crate) fn reset_render_order(&self) { @@ -70,20 +79,20 @@ impl Tree { pub(crate) fn reset_child_layouts(&self, parent: WidgetId) { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); - let children = data.nodes[parent.0].children.clone(); + let children = data.nodes[&parent].children.clone(); for child in children { - data.nodes[child.0].layout = None; + data.nodes.get_mut(&child).expect("missing widget").layout = None; } } 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 - .map(|new_hover| data.widget_hierarchy(new_hover.id, self)) + .map(|new_hover| data.widget_hierarchy(new_hover.id(), self)) .unwrap_or_default(); let unhovered = match data.update_tracked_widget(new_hover, self, |data| &mut data.hover) { Ok(Some(old_hover)) => { - let mut old_hovered = data.widget_hierarchy(old_hover.id, self); + let mut old_hovered = data.widget_hierarchy(old_hover.id(), self); // For any widgets that were shared, remove them, as they don't // need to have their events fired again. let mut new_index = 0; @@ -111,7 +120,7 @@ impl Tree { data.update_tracked_widget(new_active, self, |data| &mut data.active) } - pub fn widget(&self, id: WidgetId) -> ManagedWidget { + pub fn widget(&self, id: WidgetId) -> Option { let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); data.widget(id, self) } @@ -137,7 +146,7 @@ impl Tree { if hovered == id { return true; } - search = data.nodes[hovered.0].parent; + search = data.nodes[&hovered].parent; } false @@ -154,11 +163,10 @@ impl Tree { let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); let mut hits = Vec::new(); for id in data.render_order.iter().rev() { - if let Some(last_rendered) = data.nodes[id.0].layout { + if let Some(last_rendered) = data.nodes[id].layout { if last_rendered.contains(point) { hits.push(ManagedWidget { - id: *id, - widget: data.nodes[id.0].widget.clone(), + widget: data.nodes[id].widget.clone(), tree: self.clone(), }); } @@ -169,12 +177,12 @@ impl Tree { pub(crate) fn parent(&self, id: WidgetId) -> Option { let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); - data.nodes[id.0].parent + data.nodes.get(&id).expect("missing widget").parent } pub(crate) fn attach_styles(&self, id: WidgetId, styles: Styles) { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); - data.nodes[id.0].styles = Some(styles); + data.nodes.get_mut(&id).expect("missing widget").styles = Some(styles); } pub fn query_styles( @@ -185,7 +193,7 @@ impl Tree { self.data .lock() .map_or_else(PoisonError::into_inner, |g| g) - .query_styles(perspective.id, query) + .query_styles(perspective.id(), query) } pub fn query_style( @@ -196,7 +204,7 @@ impl Tree { self.data .lock() .map_or_else(PoisonError::into_inner, |g| g) - .query_style(perspective.id, component) + .query_style(perspective.id(), component) } } @@ -207,7 +215,7 @@ pub(crate) struct HoverResults { #[derive(Default)] struct TreeData { - nodes: Lots, + nodes: HashMap, active: Option, focus: Option, hover: Option, @@ -215,17 +223,16 @@ struct TreeData { } impl TreeData { - fn widget(&self, id: WidgetId, tree: &Tree) -> ManagedWidget { - ManagedWidget { - id, - widget: self.nodes[id.0].widget.clone(), + fn widget(&self, id: WidgetId, tree: &Tree) -> Option { + Some(ManagedWidget { + widget: self.nodes.get(&id)?.widget.clone(), tree: tree.clone(), - } + }) } fn remove_child(&mut self, child: WidgetId, parent: WidgetId) { - let removed_node = self.nodes.remove(child.0).expect("widget already removed"); - let parent = &mut self.nodes[parent.0]; + let removed_node = self.nodes.remove(&child).expect("widget already removed"); + let parent = self.nodes.get_mut(&parent).expect("missing widget"); let index = parent .children .iter() @@ -236,16 +243,16 @@ impl TreeData { let mut detached_nodes = removed_node.children; while let Some(node) = detached_nodes.pop() { - let mut node = self.nodes.remove(node.0).expect("detached node missing"); + let mut node = self.nodes.remove(&node).expect("detached node missing"); detached_nodes.append(&mut node.children); } } pub(crate) fn widget_hierarchy(&self, mut widget: WidgetId, tree: &Tree) -> Vec { let mut hierarchy = Vec::new(); - loop { - hierarchy.push(self.widget(widget, tree)); - let Some(parent) = self.nodes[widget.0].parent else { + while let Some(managed) = self.widget(widget, tree) { + hierarchy.push(managed); + let Some(parent) = self.nodes[&widget].parent else { break; }; widget = parent; @@ -263,13 +270,12 @@ impl TreeData { property: impl FnOnce(&mut Self) -> &mut Option, ) -> Result, ()> { match ( - mem::replace(property(self), new_widget.map(|w| w.id)), + mem::replace(property(self), new_widget.map(ManagedWidget::id)), new_widget, ) { - (Some(old_widget), Some(new_widget)) if old_widget == new_widget.id => Err(()), + (Some(old_widget), Some(new_widget)) if old_widget == new_widget.id() => Err(()), (Some(old_widget), _) => Ok(Some(ManagedWidget { - id: old_widget, - widget: self.nodes[old_widget.0].widget.clone(), + widget: self.nodes[&old_widget].widget.clone(), tree: tree.clone(), })), (None, _) => Ok(None), @@ -284,7 +290,7 @@ impl TreeData { let mut query = query.iter().map(|n| n.name()).collect::>(); let mut resolved = Styles::new(); while !query.is_empty() { - let node = &self.nodes[perspective.0]; + let node = &self.nodes[&perspective]; if let Some(styles) = &node.styles { query.retain(|name| { if let Some(component) = styles.get(name) { @@ -308,7 +314,7 @@ impl TreeData { ) -> Component::ComponentType { let name = query.name(); loop { - let node = &self.nodes[perspective.0]; + let node = &self.nodes[&perspective]; if let Some(styles) = &node.styles { if let Some(component) = styles.get(&name) { let Ok(value) = @@ -334,5 +340,12 @@ pub struct Node { pub styles: Option, } -#[derive(Clone, Copy, Eq, PartialEq, Debug)] -pub struct WidgetId(LotId); +#[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/value.rs b/src/value.rs index 3c5064b..91b8424 100644 --- a/src/value.rs +++ b/src/value.rs @@ -479,7 +479,7 @@ impl Value { } /// Returns a clone of the currently stored value. - pub fn get(&mut self) -> T + pub fn get(&self) -> T where T: Clone, { diff --git a/src/widget.rs b/src/widget.rs index c27d16a..54a1a7f 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -184,7 +184,7 @@ pub trait MakeWidget: Sized { /// /// Gooey automatically determines reverse tab order by using this same /// relationship. - fn with_next_focus(self, next_focus: impl IntoValue>) -> WidgetInstance { + fn with_next_focus(self, next_focus: impl IntoValue>) -> WidgetInstance { self.make_widget().with_next_focus(next_focus) } } @@ -242,8 +242,9 @@ where /// An instance of a [`Widget`]. #[derive(Clone, Debug)] pub struct WidgetInstance { + id: WidgetId, widget: Arc>, - next_focus: Value>>>, + next_focus: Value>, } impl WidgetInstance { @@ -253,11 +254,18 @@ impl WidgetInstance { W: Widget, { Self { + id: WidgetId::unique(), widget: Arc::new(Mutex::new(widget)), next_focus: Value::default(), } } + /// Returns the unique id of this widget instance. + #[must_use] + pub fn id(&self) -> WidgetId { + self.id + } + /// Sets the widget that should be focused next. /// /// Gooey automatically determines reverse tab order by using this same @@ -265,17 +273,9 @@ impl WidgetInstance { #[must_use] pub fn with_next_focus( mut self, - next_focus: impl IntoValue>, + next_focus: impl IntoValue>, ) -> WidgetInstance { - self.next_focus = match next_focus.into_value() { - Value::Constant(maybe_widget) => { - Value::Constant(maybe_widget.map(|widget| widget.widget)) - } - Value::Dynamic(dynamic) => Value::Dynamic( - dynamic - .map_each(|instance| instance.as_ref().map(|instance| instance.widget.clone())), - ), - }; + self.next_focus = next_focus.into_value(); self } @@ -361,7 +361,6 @@ where /// A [`Widget`] that has been attached to a widget hierarchy. #[derive(Clone)] pub struct ManagedWidget { - pub(crate) id: WidgetId, pub(crate) widget: WidgetInstance, pub(crate) tree: Tree, } @@ -369,7 +368,6 @@ pub struct ManagedWidget { impl Debug for ManagedWidget { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ManagedWidget") - .field("id", &self.id) .field("widget", &self.widget) .finish_non_exhaustive() } @@ -385,51 +383,71 @@ impl ManagedWidget { } pub(crate) fn set_layout(&self, rect: Rect) { - self.tree.set_layout(self.id, rect); + self.tree.set_layout(self.id(), rect); + } + + /// Returns the unique id of this widget instance. + #[must_use] + pub fn id(&self) -> WidgetId { + self.widget.id + } + + /// Returns the next widget to focus after this widget. + /// + /// This function returns the value set in + /// [`MakeWidget::with_next_focus()`]. + #[must_use] + pub fn next_focus(&self) -> Option { + self.widget + .next_focus + .get() + .and_then(|next_focus| self.tree.widget(next_focus)) } /// Returns the region that the widget was last rendered at. #[must_use] pub fn last_layout(&self) -> Option> { - self.tree.layout(self.id) + self.tree.layout(self.id()) } /// Returns true if this widget is the currently active widget. #[must_use] pub fn active(&self) -> bool { - self.tree.active_widget() == Some(self.id) + self.tree.active_widget() == Some(self.id()) } /// Returns true if this widget is currently the hovered widget. #[must_use] pub fn hovered(&self) -> bool { - self.tree.is_hovered(self.id) + self.tree.is_hovered(self.id()) } /// Returns true if this widget that is directly beneath the cursor. #[must_use] pub fn primary_hover(&self) -> bool { - self.tree.hovered_widget() == Some(self.id) + self.tree.hovered_widget() == Some(self.id()) } /// Returns true if this widget is the currently focused widget. #[must_use] pub fn focused(&self) -> bool { - self.tree.focused_widget() == Some(self.id) + self.tree.focused_widget() == Some(self.id()) } /// Returns the parent of this widget. #[must_use] pub fn parent(&self) -> Option { - self.tree.parent(self.id).map(|id| self.tree.widget(id)) + self.tree + .parent(self.id()) + .and_then(|id| self.tree.widget(id)) } pub(crate) fn attach_styles(&self, styles: Styles) { - self.tree.attach_styles(self.id, styles); + self.tree.attach_styles(self.id(), styles); } pub(crate) fn reset_child_layouts(&self) { - self.tree.reset_child_layouts(self.id); + self.tree.reset_child_layouts(self.id()); } } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 4965e40..2f9b14d 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -148,6 +148,11 @@ 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. + true + } + fn mouse_down( &mut self, _location: Point, diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 15c0a50..0a6e3c3 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -113,6 +113,10 @@ impl Widget for Input { true } + fn accept_focus(&mut self, _context: &mut EventContext<'_, '_>) -> bool { + true + } + fn mouse_down( &mut self, location: Point, diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 0c646f7..accc6df 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -65,11 +65,11 @@ impl Widget for Label { self.text.map(|contents| { let measured = context .graphics - .measure_text(Text::from(contents).wrap_at(dbg!(width))); + .measure_text(Text::from(contents).wrap_at(width)); let mut size = measured.size.try_cast().unwrap_or_default(); size += padding * 2; self.prepared_text = Some(measured); - dbg!(size) + size }) } } diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index a220ea1..44dc5ad 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -34,7 +34,7 @@ impl Stack { direction: impl IntoValue, widgets: impl IntoValue, ) -> Self { - let mut direction = direction.into_value(); + let direction = direction.into_value(); let initial_direction = direction.get(); diff --git a/src/window.rs b/src/window.rs index fab8104..78dea16 100644 --- a/src/window.rs +++ b/src/window.rs @@ -21,7 +21,8 @@ use kludgine::render::Drawing; use kludgine::Kludgine; use crate::context::{ - EventContext, Exclusive, GraphicsContext, LayoutContext, RedrawStatus, WidgetContext, + AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, RedrawStatus, + WidgetContext, }; use crate::graphics::Graphics; use crate::tree::Tree; @@ -154,6 +155,7 @@ struct GooeyWindow { should_close: bool, mouse_state: MouseState, redraw_status: RedrawStatus, + initial_frame: bool, } impl GooeyWindow @@ -192,6 +194,7 @@ where devices: HashMap::default(), }, redraw_status: RedrawStatus::default(), + initial_frame: true, } } @@ -213,6 +216,12 @@ where let render_size = actual_size.min(window_size); self.root.set_layout(Rect::from(render_size.into_signed())); + if self.initial_frame { + self.initial_frame = false; + layout_context.focus(); + layout_context.as_event_context().apply_pending_state(); + } + if render_size.width < window_size.width || render_size.height < window_size.height { layout_context .clipped_to(Rect::from(render_size.into_signed())) @@ -285,8 +294,8 @@ where input: KeyEvent, is_synthetic: bool, ) { - let target = self.root.tree.focused_widget().unwrap_or(self.root.id); - let target = self.root.tree.widget(target); + 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, @@ -320,9 +329,18 @@ where delta: MouseScrollDelta, phase: TouchPhase, ) { - let widget = self.root.tree.hovered_widget().unwrap_or(self.root.id); + let widget = self + .root + .tree + .hovered_widget() + .and_then(|hovered| self.root.tree.widget(hovered)) + .unwrap_or_else(|| { + self.root + .tree + .widget(self.root.id()) + .expect("missing widget") + }); - let widget = self.root.tree.widget(widget); let mut widget = EventContext::new( WidgetContext::new(widget, &self.redraw_status, &mut window), kludgine, @@ -335,10 +353,19 @@ where // fn modifiers_changed(&mut self, window: kludgine::app::Window<'_, ()>) {} fn ime(&mut self, mut window: RunningWindow<'_>, kludgine: &mut Kludgine, ime: Ime) { - let target = self.root.tree.focused_widget().unwrap_or(self.root.id); - let target = self.root.tree.widget(target); + let widget = self + .root + .tree + .focused_widget() + .and_then(|hovered| self.root.tree.widget(hovered)) + .unwrap_or_else(|| { + self.root + .tree + .widget(self.root.id()) + .expect("missing widget") + }); let mut target = EventContext::new( - WidgetContext::new(target, &self.redraw_status, &mut window), + WidgetContext::new(widget, &self.redraw_status, &mut window), kludgine, );