diff --git a/Cargo.lock b/Cargo.lock index 12ee340..6fff768 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -969,7 +969,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.1.0" -source = "git+https://github.com/khonsulabs/kludgine#a26299823498dccbbbb3c28abc820b660fcc1289" +source = "git+https://github.com/khonsulabs/kludgine#09790aafb5a9c3b0da034387adead9960eb06bc7" dependencies = [ "ahash", "alot", @@ -1807,9 +1807,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "ffb93593068e9babdad10e4fce47dc9b3ac25315a72a59766ffd9e9a71996a04" dependencies = [ "bitflags 2.4.1", "errno", @@ -2031,9 +2031,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] diff --git a/examples/containers.rs b/examples/containers.rs index 2f2c363..3a703e0 100644 --- a/examples/containers.rs +++ b/examples/containers.rs @@ -6,7 +6,7 @@ use gooey::Run; fn main() -> gooey::Result { let theme_mode = Dynamic::default(); - set_of_containers(1, theme_mode.clone()) + set_of_containers(3, theme_mode.clone()) .centered() .into_window() .with_theme_mode(theme_mode) diff --git a/src/animation.rs b/src/animation.rs index ba99798..7a55f08 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -43,7 +43,7 @@ use std::fmt::{Debug, Display}; use std::ops::{ControlFlow, Deref, Div, Mul}; use std::panic::{RefUnwindSafe, UnwindSafe}; use std::str::FromStr; -use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock}; use std::thread; use std::time::{Duration, Instant}; @@ -54,7 +54,8 @@ use kludgine::figures::Ranged; use kludgine::Color; use crate::animation::easings::Linear; -use crate::styles::Component; +use crate::styles::{Component, RequireInvalidation}; +use crate::utils::IgnorePoison; use crate::value::Dynamic; static ANIMATIONS: Mutex = Mutex::new(Animating::new()); @@ -65,9 +66,7 @@ fn thread_state() -> MutexGuard<'static, Animating> { THREAD.get_or_init(|| { thread::spawn(animation_thread); }); - ANIMATIONS - .lock() - .map_or_else(PoisonError::into_inner, |g| g) + ANIMATIONS.lock().ignore_poison() } fn animation_thread() { @@ -75,9 +74,7 @@ fn animation_thread() { loop { if state.running.is_empty() { state.last_updated = None; - state = NEW_ANIMATIONS - .wait(state) - .map_or_else(PoisonError::into_inner, |g| g); + state = NEW_ANIMATIONS.wait(state).ignore_poison(); } else { let start = Instant::now(); let last_tick = state.last_updated.unwrap_or(start); @@ -641,9 +638,9 @@ macro_rules! impl_lerp_for_uint { fn lerp(&self, target: &Self, percent: f32) -> Self { let percent = $float::from(percent); if let Some(delta) = target.checked_sub(*self) { - *self + (delta as $float * percent).round() as $type + self.saturating_add((delta as $float * percent).round() as $type) } else { - *self - ((*self - *target) as $float * percent).round() as $type + self.saturating_sub(((*self - *target) as $float * percent).round() as $type) } } } @@ -701,8 +698,10 @@ impl PercentBetween for bool { fn integer_lerps() { #[track_caller] fn test_lerps(a: &T, b: &T, mid: &T) { + assert_eq!(&b.lerp(a, 1.), a); assert_eq!(&a.lerp(b, 1.), b); assert_eq!(&a.lerp(b, 0.), a); + assert_eq!(&b.lerp(a, 0.), b); assert_eq!(&a.lerp(b, 0.5), mid); } @@ -1012,6 +1011,12 @@ impl TryFrom for EasingFunction { } } +impl RequireInvalidation for EasingFunction { + fn requires_invalidation(&self) -> bool { + false + } +} + /// Performs easing for value interpolation. pub trait Easing: Debug + Send + Sync + RefUnwindSafe + UnwindSafe + 'static { /// Eases a value ranging between zero and one. The resulting value does not diff --git a/src/context.rs b/src/context.rs index f0555db..e487a18 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,9 +1,11 @@ //! Types that provide access to the Gooey runtime. use std::borrow::Cow; +use std::hash::Hash; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Mutex, MutexGuard}; +use kempt::Set; use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; @@ -15,6 +17,7 @@ use kludgine::{Color, Kludgine}; use crate::graphics::Graphics; use crate::styles::components::{HighlightColor, WidgetBackground}; use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair, VisualOrder}; +use crate::utils::IgnorePoison; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef}; use crate::window::sealed::WindowCommand; @@ -572,14 +575,23 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> LayoutContext<'context, 'window, 'cl /// context's widget and returns the result. pub fn layout(&mut self, available_space: Size) -> Size { if self.persist_layout { - self.graphics.current_node.reset_child_layouts(); + if let Some(cached) = self.graphics.current_node.begin_layout(available_space) { + return cached; + } } - self.graphics + let result = self + .graphics .current_node .clone() .lock() .as_widget() - .layout(available_space, self) + .layout(available_space, self); + if self.persist_layout { + self.graphics + .current_node + .persist_layout(available_space, result); + } + result } /// Sets the layout for `child` to `layout`. @@ -664,7 +676,7 @@ impl<'window> AsEventContext<'window> for GraphicsContext<'_, 'window, '_, '_, ' /// specific widget. pub struct WidgetContext<'context, 'window> { current_node: ManagedWidget, - redraw_status: &'context RedrawStatus, + redraw_status: &'context InvalidationStatus, window: &'context mut RunningWindow<'window>, theme: Cow<'context, ThemePair>, pending_state: PendingState<'context>, @@ -675,7 +687,7 @@ pub struct WidgetContext<'context, 'window> { impl<'context, 'window> WidgetContext<'context, 'window> { pub(crate) fn new( current_node: ManagedWidget, - redraw_status: &'context RedrawStatus, + redraw_status: &'context InvalidationStatus, theme: &'context ThemePair, window: &'context mut RunningWindow<'window>, theme_mode: ThemeMode, @@ -755,6 +767,11 @@ impl<'context, 'window> WidgetContext<'context, 'window> { value.redraw_when_changed(self.handle()); } + /// Ensures that this widget will be redrawn when `value` has been updated. + pub fn invalidate_when_changed(&self, value: &Dynamic) { + value.invalidate_when_changed(self.handle(), self.current_node.id()); + } + /// Returns the last layout of this widget. #[must_use] pub fn last_layout(&self) -> Option> { @@ -963,7 +980,24 @@ impl<'context, 'window> WidgetContext<'context, 'window> { pub(crate) struct WindowHandle { kludgine: kludgine::app::WindowHandle, - redraw_status: RedrawStatus, + redraw_status: InvalidationStatus, +} + +impl Eq for WindowHandle {} + +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq( + &self.redraw_status.invalidated, + &other.redraw_status.invalidated, + ) + } +} + +impl Hash for WindowHandle { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.redraw_status.invalidated).hash(state); + } } impl WindowHandle { @@ -972,6 +1006,12 @@ impl WindowHandle { let _result = self.kludgine.send(WindowCommand::Redraw); } } + + pub fn invalidate(&self, widget: WidgetId) { + if self.redraw_status.invalidate(widget) { + self.redraw(); + } + } } impl dyn AsEventContext<'_> {} @@ -1036,11 +1076,12 @@ impl DerefMut for PendingState<'_> { } #[derive(Default, Clone)] -pub(crate) struct RedrawStatus { +pub(crate) struct InvalidationStatus { refresh_sent: Arc, + invalidated: Arc>>, } -impl RedrawStatus { +impl InvalidationStatus { pub fn should_send_refresh(&self) -> bool { self.refresh_sent .compare_exchange(false, true, Ordering::Release, Ordering::Acquire) @@ -1050,6 +1091,15 @@ impl RedrawStatus { pub fn refresh_received(&self) { self.refresh_sent.store(false, Ordering::Release); } + + pub fn invalidate(&self, widget: WidgetId) -> bool { + let mut invalidated = self.invalidated.lock().ignore_poison(); + invalidated.insert(widget) + } + + pub fn invalidations(&self) -> MutexGuard<'_, Set> { + self.invalidated.lock().ignore_poison() + } } /// A type chat can convert to a [`ManagedWidget`] through a [`WidgetContext`]. diff --git a/src/styles.rs b/src/styles.rs index 650e9d6..4446f97 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -86,8 +86,17 @@ impl Styles { self.0 .get(&name) .and_then(|component| { - component.redraw_when_changed(context); - ::try_from_component(component.get()).ok() + match ::try_from_component(component.get()) { + Ok(value) => { + if value.requires_invalidation() { + component.invalidate_when_changed(context); + } else { + component.redraw_when_changed(context); + } + Some(value) + } + Err(_) => None, + } }) .unwrap_or_else(|| component.default_value(context)) } @@ -219,6 +228,12 @@ impl TryFrom for Color { } } +impl RequireInvalidation for Color { + fn requires_invalidation(&self) -> bool { + false + } +} + impl From for Component { fn from(value: Dimension) -> Self { Self::Dimension(value) @@ -236,6 +251,12 @@ impl TryFrom for Dimension { } } +impl RequireInvalidation for Dimension { + fn requires_invalidation(&self) -> bool { + true + } +} + impl From for Component { fn from(value: Px) -> Self { Self::from(Dimension::from(value)) @@ -253,6 +274,12 @@ impl TryFrom for Px { } } +impl RequireInvalidation for Px { + fn requires_invalidation(&self) -> bool { + true + } +} + impl From for Component { fn from(value: Lp) -> Self { Self::from(Dimension::from(value)) @@ -270,6 +297,12 @@ impl TryFrom for Lp { } } +impl RequireInvalidation for Lp { + fn requires_invalidation(&self) -> bool { + true + } +} + /// A 1-dimensional measurement that may be automatically calculated. #[derive(Debug, Clone, Copy)] pub enum FlexibleDimension { @@ -563,6 +596,12 @@ impl TryFrom for DimensionRange { } } +impl RequireInvalidation for DimensionRange { + fn requires_invalidation(&self) -> bool { + true + } +} + /// A custom component value. #[derive(Debug, Clone)] pub struct CustomComponent(Arc); @@ -571,7 +610,7 @@ impl CustomComponent { /// Wraps an arbitrary value so that it can be used as a [`Component`]. pub fn new(value: T) -> Self where - T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static, + T: RequireInvalidation + RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static, { Self(Arc::new(value)) } @@ -587,6 +626,12 @@ impl CustomComponent { } } +impl RequireInvalidation for CustomComponent { + fn requires_invalidation(&self) -> bool { + self.0.requires_invalidation() + } +} + impl ComponentType for CustomComponent { fn into_component(self) -> Component { Component::Custom(self) @@ -600,13 +645,13 @@ impl ComponentType for CustomComponent { } } -trait AnyComponent: Send + Sync + RefUnwindSafe + UnwindSafe + Debug { +trait AnyComponent: RequireInvalidation + Send + Sync + RefUnwindSafe + UnwindSafe + Debug { fn as_any(&self) -> &dyn Any; } impl AnyComponent for T where - T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static, + T: RequireInvalidation + RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static, { fn as_any(&self) -> &dyn Any { self @@ -654,8 +699,20 @@ pub trait ComponentDefinition: NamedComponent { fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType; } +/// Describes whether a type should invalidate a widget. +pub trait RequireInvalidation { + /// Gooey tracks two different states: + /// + /// - Whether to repaint the window + /// - Whether to relayout a widget + /// + /// If a value change of `self` may require a relayout, this should return + /// true. + fn requires_invalidation(&self) -> bool; +} + /// A type that can be converted to and from [`Component`]. -pub trait ComponentType: Sized { +pub trait ComponentType: RequireInvalidation + Sized { /// Returns this type, wrapped in a [`Component`]. fn into_component(self) -> Component; /// Attempts to extract this type from `component`. If `component` does not @@ -665,7 +722,7 @@ pub trait ComponentType: Sized { impl ComponentType for T where - T: Into + TryFrom, + T: RequireInvalidation + Into + TryFrom, { fn into_component(self) -> Component { self.into() @@ -1411,6 +1468,12 @@ impl TryFrom for VisualOrder { } } +impl RequireInvalidation for VisualOrder { + fn requires_invalidation(&self) -> bool { + true + } +} + /// A horizontal direction. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum HorizontalOrder { @@ -1514,6 +1577,12 @@ impl TryFrom for FocusableWidgets { } } +impl RequireInvalidation for FocusableWidgets { + fn requires_invalidation(&self) -> bool { + false + } +} + /// A description of the level of depth a /// [`Container`](crate::widgets::Container) is nested at. #[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] @@ -1563,6 +1632,12 @@ impl TryFrom for ContainerLevel { } } +impl RequireInvalidation for ContainerLevel { + fn requires_invalidation(&self) -> bool { + true + } +} + /// A builder of [`ColorScheme`]s. #[derive(Clone, Copy, Debug)] pub struct ColorSchemeBuilder { diff --git a/src/tick.rs b/src/tick.rs index d57ed02..ad3f6c7 100644 --- a/src/tick.rs +++ b/src/tick.rs @@ -1,12 +1,13 @@ use std::collections::HashSet; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard}; use std::time::{Duration, Instant}; use kludgine::app::winit::event::KeyEvent; use kludgine::app::winit::keyboard::Key; use crate::context::WidgetContext; +use crate::utils::IgnorePoison; use crate::value::Dynamic; use crate::widget::{EventHandling, HANDLED, IGNORED}; @@ -123,9 +124,7 @@ struct TickData { impl TickData { fn state(&self) -> MutexGuard<'_, TickState> { - self.state - .lock() - .map_or_else(PoisonError::into_inner, |g| g) + self.state.lock().ignore_poison() } } @@ -173,10 +172,7 @@ where while state.keep_running { let current_frame = data.rendered_frame.load(Ordering::Acquire); if state.frame == current_frame { - state = data - .sync - .wait(state) - .map_or_else(PoisonError::into_inner, |g| g); + state = data.sync.wait(state).ignore_poison(); } else { break; } diff --git a/src/tree.rs b/src/tree.rs index 58c5e71..927e69b 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -1,15 +1,17 @@ use std::mem; -use std::sync::{Arc, Mutex, PoisonError}; +use std::sync::{Arc, Mutex}; use ahash::AHashMap; use alot::{LotId, Lots}; -use kludgine::figures::units::Px; -use kludgine::figures::{Point, Rect}; +use kludgine::figures::units::{Px, UPx}; +use kludgine::figures::{Point, Rect, Size}; use crate::styles::{Styles, ThemePair, VisualOrder}; +use crate::utils::IgnorePoison; use crate::value::Value; use crate::widget::{ManagedWidget, WidgetId, WidgetInstance}; use crate::window::ThemeMode; +use crate::ConstraintLimit; #[derive(Clone, Default)] pub struct Tree { @@ -22,7 +24,7 @@ impl Tree { widget: WidgetInstance, parent: Option<&ManagedWidget>, ) -> ManagedWidget { - let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let mut data = self.data.lock().ignore_poison(); let id = widget.id(); let (effective_styles, parent_id) = if let Some(parent) = parent { ( @@ -36,6 +38,7 @@ impl Tree { widget: widget.clone(), children: Vec::new(), parent: parent_id, + last_layout_query: None, layout: None, associated_styles: None, effective_styles, @@ -68,7 +71,7 @@ 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); + let mut data = self.data.lock().ignore_poison(); data.remove_child(child.node_id, parent.node_id); if child.widget.is_default() { @@ -80,7 +83,7 @@ impl Tree { } pub(crate) fn set_layout(&self, widget: LotId, rect: Rect) { - let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let mut data = self.data.lock().ignore_poison(); let node = &mut data.nodes[widget]; node.layout = Some(rect); @@ -98,26 +101,62 @@ impl Tree { } pub(crate) fn layout(&self, widget: LotId) -> Option> { - let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let data = self.data.lock().ignore_poison(); data.nodes.get(widget).and_then(|widget| widget.layout) } - pub(crate) fn reset_render_order(&self) { - let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + pub(crate) fn new_frame(&self, invalidations: impl IntoIterator) { + let mut data = self.data.lock().ignore_poison(); data.render_order.clear(); + + for id in invalidations { + let Some(id) = data.nodes_by_id.get(&id).copied() else { + continue; + }; + + data.invalidate(id, true); + } } pub(crate) fn note_widget_rendered(&self, widget: LotId) { - let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let mut data = self.data.lock().ignore_poison(); data.render_order.push(widget); } - pub(crate) fn reset_child_layouts(&self, parent: LotId) { - let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); - let children = data.nodes[parent].children.clone(); - for child in children { - data.nodes.get_mut(child).expect("missing widget").layout = None; + pub(crate) fn begin_layout( + &self, + parent: LotId, + constraints: Size, + ) -> Option> { + let mut data = self.data.lock().ignore_poison(); + + let node = &mut data.nodes[parent]; + if let Some(cached_layout) = &node.last_layout_query { + if constraints.width.max() < cached_layout.constraints.width.max() + && constraints.height.max() < cached_layout.constraints.height.max() + { + return Some(cached_layout.size); + } + + node.last_layout_query = None; } + + let children = node.children.clone(); + for child in children { + data.invalidate(child, false); + } + + None + } + + pub(crate) fn persist_layout( + &self, + id: LotId, + constraints: Size, + size: Size, + ) { + let mut data = self.data.lock().ignore_poison(); + data.nodes[id].last_layout_query = Some(CachedLayoutQuery { constraints, size }); } pub(crate) fn visually_ordered_children( @@ -125,7 +164,7 @@ impl Tree { parent: LotId, order: VisualOrder, ) -> Vec { - let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let data = self.data.lock().ignore_poison(); let node = &data.nodes[parent]; let mut unordered = node.children.clone(); let mut ordered = Vec::::with_capacity(unordered.len()); @@ -182,12 +221,12 @@ impl Tree { } pub(crate) fn effective_styles(&self, id: LotId) -> Styles { - let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let data = self.data.lock().ignore_poison(); data.nodes[id].effective_styles.clone() } 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 mut data = self.data.lock().ignore_poison(); let hovered = new_hover .map(|new_hover| data.widget_hierarchy(new_hover.node_id, self)) .unwrap_or_default(); @@ -209,7 +248,7 @@ impl Tree { } pub fn focus(&self, new_focus: Option<&ManagedWidget>) -> Result, ()> { - let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let mut data = self.data.lock().ignore_poison(); data.update_tracked_widget(new_focus, self, |data| &mut data.focus) } @@ -217,54 +256,38 @@ impl Tree { &self, new_active: Option<&ManagedWidget>, ) -> Result, ()> { - let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let mut data = self.data.lock().ignore_poison(); data.update_tracked_widget(new_active, self, |data| &mut data.active) } pub fn widget(&self, id: WidgetId) -> Option { - let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let data = self.data.lock().ignore_poison(); data.widget_from_id(id, self) } pub(crate) fn widget_from_node(&self, id: LotId) -> Option { - let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let data = self.data.lock().ignore_poison(); data.widget_from_node(id, self) } pub(crate) fn active_widget(&self) -> Option { - self.data - .lock() - .map_or_else(PoisonError::into_inner, |g| g) - .active + self.data.lock().ignore_poison().active } pub(crate) fn hovered_widget(&self) -> Option { - self.data - .lock() - .map_or_else(PoisonError::into_inner, |g| g) - .hover + self.data.lock().ignore_poison().hover } pub(crate) fn default_widget(&self) -> Option { - self.data - .lock() - .map_or_else(PoisonError::into_inner, |g| g) - .defaults - .last() - .copied() + self.data.lock().ignore_poison().defaults.last().copied() } pub(crate) fn escape_widget(&self) -> Option { - self.data - .lock() - .map_or_else(PoisonError::into_inner, |g| g) - .escapes - .last() - .copied() + self.data.lock().ignore_poison().escapes.last().copied() } pub(crate) fn is_hovered(&self, id: LotId) -> bool { - let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let data = self.data.lock().ignore_poison(); let mut search = data.hover; while let Some(hovered) = search { if hovered == id { @@ -277,14 +300,11 @@ impl Tree { } pub(crate) fn focused_widget(&self) -> Option { - self.data - .lock() - .map_or_else(PoisonError::into_inner, |g| g) - .focus + self.data.lock().ignore_poison().focus } pub(crate) fn widgets_at_point(&self, point: Point) -> Vec { - let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let data = self.data.lock().ignore_poison(); let mut hits = Vec::new(); for id in data.render_order.iter().rev() { if let Some(last_rendered) = data.nodes.get(*id).and_then(|widget| widget.layout) { @@ -297,22 +317,22 @@ impl Tree { } pub(crate) fn parent(&self, id: LotId) -> Option { - let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let data = self.data.lock().ignore_poison(); data.nodes.get(id).expect("missing widget").parent } pub(crate) fn attach_styles(&self, id: LotId, styles: Value) { - let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let mut data = self.data.lock().ignore_poison(); data.attach_styles(id, styles); } pub(crate) fn attach_theme(&self, id: LotId, theme: Value) { - let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let mut data = self.data.lock().ignore_poison(); data.nodes.get_mut(id).expect("missing widget").theme = Some(theme); } pub(crate) fn attach_theme_mode(&self, id: LotId, theme: Value) { - let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let mut data = self.data.lock().ignore_poison(); data.nodes.get_mut(id).expect("missing widget").theme_mode = Some(theme); } @@ -320,7 +340,7 @@ impl Tree { &self, id: LotId, ) -> (Styles, Option>, Option>) { - let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let data = self.data.lock().ignore_poison(); let node = data.nodes.get(id).expect("missing widget"); ( node.effective_styles.clone(), @@ -328,6 +348,13 @@ impl Tree { node.theme_mode.clone(), ) } + + pub fn invalidate(&self, id: LotId, include_hierarchy: bool) { + self.data + .lock() + .ignore_poison() + .invalidate(id, include_hierarchy); + } } pub(crate) struct HoverResults { @@ -453,17 +480,31 @@ impl TreeData { (None, _) => Ok(None), } } + + fn invalidate(&mut self, id: LotId, include_hierarchy: bool) { + let mut node = &mut self.nodes[id]; + while node.layout.is_some() { + node.layout = None; + node.last_layout_query = None; + + let (true, Some(parent)) = (include_hierarchy, node.parent) else { + break; + }; + node = &mut self.nodes[parent]; + } + } } -pub struct Node { - pub widget: WidgetInstance, - pub children: Vec, - pub parent: Option, - pub layout: Option>, - pub associated_styles: Option>, - pub effective_styles: Styles, - pub theme: Option>, - pub theme_mode: Option>, +struct Node { + widget: WidgetInstance, + children: Vec, + parent: Option, + layout: Option>, + last_layout_query: Option, + associated_styles: Option>, + effective_styles: Styles, + theme: Option>, + theme_mode: Option>, } impl Node { @@ -475,3 +516,8 @@ impl Node { effective_styles } } + +struct CachedLayoutQuery { + constraints: Size, + size: Size, +} diff --git a/src/utils.rs b/src/utils.rs index 8811dcc..228efa7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ use std::ops::Deref; -use std::sync::OnceLock; +use std::sync::{OnceLock, PoisonError}; use kludgine::app::winit::event::Modifiers; use kludgine::app::winit::keyboard::ModifiersState; @@ -129,3 +129,16 @@ impl Deref for Lazy { self.once.get_or_init(self.init) } } + +pub trait IgnorePoison { + type Unwrapped; + fn ignore_poison(self) -> Self::Unwrapped; +} + +impl IgnorePoison for Result> { + type Unwrapped = T; + + fn ignore_poison(self) -> Self::Unwrapped { + self.map_or_else(PoisonError::into_inner, |g| g) + } +} diff --git a/src/value.rs b/src/value.rs index 3a08a6a..710a84b 100644 --- a/src/value.rs +++ b/src/value.rs @@ -5,15 +5,17 @@ use std::fmt::{Debug, Display}; use std::future::Future; use std::ops::{Deref, DerefMut}; use std::panic::AssertUnwindSafe; -use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, TryLockError}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard, TryLockError}; use std::task::{Poll, Waker}; use std::thread::ThreadId; +use ahash::AHashSet; use intentional::Assert; use crate::animation::{DynamicTransition, LinearInterpolate}; use crate::context::{WidgetContext, WindowHandle}; -use crate::utils::WithClone; +use crate::utils::{IgnorePoison, WithClone}; +use crate::widget::WidgetId; /// An instance of a value that provides APIs to observe and react to its /// contents. @@ -30,9 +32,10 @@ impl Dynamic { generation: Generation::default(), }, callbacks: Vec::new(), - windows: Vec::new(), + windows: AHashSet::new(), readers: 0, wakers: Vec::new(), + widgets: AHashSet::new(), }), during_callback_state: Mutex::default(), sync: AssertUnwindSafe(Condvar::new()), @@ -158,6 +161,10 @@ impl Dynamic { self.0.redraw_when_changed(window); } + pub(crate) fn invalidate_when_changed(&self, window: WindowHandle, widget: WidgetId) { + self.0.invalidate_when_changed(window, widget); + } + /// Returns a clone of the currently contained value. /// /// # Panics @@ -181,7 +188,7 @@ impl Dynamic { /// This function panics if this value is already locked by the current /// thread. #[must_use] - pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T + pub fn get_tracking_refresh(&self, context: &WidgetContext<'_, '_>) -> T where T: Clone, { @@ -189,6 +196,23 @@ impl Dynamic { self.get() } + /// Returns a clone of the currently contained value. + /// + /// `context` will be invalidated when the value is updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + pub fn get_tracking_invalidate(&self, context: &WidgetContext<'_, '_>) -> T + where + T: Clone, + { + context.invalidate_when_changed(self); + self.get() + } + /// Returns the currently stored value, replacing the current contents with /// `T::default()`. /// @@ -409,11 +433,7 @@ struct DynamicMutexGuard<'a, T> { impl<'a, T> Drop for DynamicMutexGuard<'a, T> { fn drop(&mut self) { - let mut during_state = self - .dynamic - .during_callback_state - .lock() - .map_or_else(PoisonError::into_inner, |g| g); + let mut during_state = self.dynamic.during_callback_state.lock().ignore_poison(); *during_state = None; drop(during_state); self.dynamic.sync.notify_all(); @@ -450,10 +470,7 @@ struct DynamicData { impl DynamicData { fn state(&self) -> Result, DeadlockError> { - let mut during_sync = self - .during_callback_state - .lock() - .map_or_else(PoisonError::into_inner, |g| g); + let mut during_sync = self.during_callback_state.lock().ignore_poison(); let current_thread_id = std::thread::current().id(); let guard = loop { @@ -466,10 +483,7 @@ impl DynamicData { return Err(DeadlockError) } Some(_) => { - during_sync = self - .sync - .wait(during_sync) - .map_or_else(PoisonError::into_inner, |g| g); + during_sync = self.sync.wait(during_sync).ignore_poison(); } None => break, } @@ -487,7 +501,12 @@ impl DynamicData { pub fn redraw_when_changed(&self, window: WindowHandle) { let mut state = self.state().expect("deadlocked"); - state.windows.push(window); + state.windows.insert(window); + } + + pub fn invalidate_when_changed(&self, window: WindowHandle, widget: WidgetId) { + let mut state = self.state().expect("deadlocked"); + state.widgets.insert((window, widget)); } pub fn get(&self) -> Result, DeadlockError> @@ -579,7 +598,8 @@ impl Display for DeadlockError { struct State { wrapped: GenerationalValue, callbacks: Vec>>, - windows: Vec, + windows: AHashSet, + widgets: AHashSet<(WindowHandle, WidgetId)>, wakers: Vec, readers: usize, } @@ -591,7 +611,10 @@ impl State { for callback in &mut self.callbacks { callback.update(&self.wrapped); } - for window in self.windows.drain(..) { + for (window, widget) in self.widgets.drain() { + window.invalidate(widget); + } + for window in self.windows.drain() { window.redraw(); } for waker in self.wakers.drain(..) { @@ -726,11 +749,7 @@ impl DynamicReader { /// This function panics if this value is already locked by the current /// thread. pub fn block_until_updated(&mut self) -> bool { - let mut deadlock_state = self - .source - .during_callback_state - .lock() - .map_or_else(PoisonError::into_inner, |g| g); + let mut deadlock_state = self.source.during_callback_state.lock().ignore_poison(); assert!( deadlock_state .as_ref() @@ -739,11 +758,7 @@ impl DynamicReader { "deadlocked" ); loop { - let state = self - .source - .state - .lock() - .map_or_else(PoisonError::into_inner, |g| g); + let state = self.source.state.lock().ignore_poison(); if state.wrapped.generation != self.read_generation { return true; } else if state.readers == Arc::strong_count(&self.source) { @@ -752,11 +767,7 @@ impl DynamicReader { drop(state); // Wait for a notification of a change, which is synch - deadlock_state = self - .source - .sync - .wait(deadlock_state) - .map_or_else(PoisonError::into_inner, |g| g); + deadlock_state = self.source.sync.wait(deadlock_state).ignore_poison(); } } @@ -936,7 +947,11 @@ impl Value { /// /// If `self` is a dynamic, `context` will be invalidated when the value is /// updated. - pub fn map_tracked(&self, context: &WidgetContext<'_, '_>, map: impl FnOnce(&T) -> R) -> R { + pub fn map_tracking_redraw( + &self, + context: &WidgetContext<'_, '_>, + map: impl FnOnce(&T) -> R, + ) -> R { match self { Value::Constant(value) => map(value), Value::Dynamic(dynamic) => { @@ -946,6 +961,24 @@ impl Value { } } + /// Maps the current contents to `map` and returns the result. + /// + /// If `self` is a dynamic, `context` will be invalidated when the value is + /// updated. + pub fn map_tracking_invalidate( + &self, + context: &WidgetContext<'_, '_>, + map: impl FnOnce(&T) -> R, + ) -> R { + match self { + Value::Constant(value) => map(value), + Value::Dynamic(dynamic) => { + context.invalidate_when_changed(dynamic); + dynamic.map_ref(map) + } + } + } + /// Maps the current contents with exclusive access and returns the result. pub fn map_mut(&mut self, map: impl FnOnce(&mut T) -> R) -> R { match self { @@ -984,7 +1017,7 @@ impl Value { where T: Clone, { - self.map_tracked(context, Clone::clone) + self.map_tracking_redraw(context, Clone::clone) } /// Returns the current generation of the data stored, if the contained @@ -1004,6 +1037,15 @@ impl Value { context.redraw_when_changed(dynamic); } } + + /// Marks the widget for redraw when this value is updated. + /// + /// This function has no effect if the value is constant. + pub fn invalidate_when_changed(&self, context: &WidgetContext<'_, '_>) { + if let Value::Dynamic(dynamic) = self { + context.invalidate_when_changed(dynamic); + } + } } impl Clone for Value where @@ -1155,7 +1197,7 @@ macro_rules! impl_tuple_for_each { move |$var: &$type| { $(let $rvar = $rvar.lock();)+ let mut for_each = - for_each.lock().map_or_else(PoisonError::into_inner, |g| g); + for_each.lock().ignore_poison(); (for_each)(($(&$avar,)+)); } })); diff --git a/src/widget.rs b/src/widget.rs index 506e338..bd1c4ff 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -6,7 +6,7 @@ use std::fmt::Debug; use std::ops::{ControlFlow, Deref, DerefMut}; use std::panic::UnwindSafe; use std::sync::atomic::{self, AtomicU64}; -use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; +use std::sync::{Arc, Mutex, MutexGuard}; use alot::LotId; use kludgine::app::winit::event::{ @@ -22,6 +22,7 @@ use crate::styles::{ ThemePair, VisualOrder, }; use crate::tree::Tree; +use crate::utils::IgnorePoison; use crate::value::{IntoValue, Value}; use crate::widgets::{Align, Container, Expand, Resize, Scroll, Stack, Style}; use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior}; @@ -908,13 +909,9 @@ impl WidgetInstance { /// Locks the widget for exclusive access. Locking widgets should only be /// done for brief moments of time when you are certain no deadlocks can /// occur due to other widget locks being held. + #[must_use] pub fn lock(&self) -> WidgetGuard<'_> { - WidgetGuard( - self.data - .widget - .lock() - .map_or_else(PoisonError::into_inner, |g| g), - ) + WidgetGuard(self.data.widget.lock().ignore_poison()) } /// Runs this widget instance as an application. @@ -1041,6 +1038,11 @@ impl ManagedWidget { self.widget.lock() } + /// Invalidates this widget. + pub fn invalidate(&self) { + self.tree.invalidate(self.node_id, false); + } + pub(crate) fn set_layout(&self, rect: Rect) { self.tree.set_layout(self.node_id, rect); } @@ -1130,8 +1132,12 @@ impl ManagedWidget { self.tree.overriden_theme(self.node_id) } - pub(crate) fn reset_child_layouts(&self) { - self.tree.reset_child_layouts(self.node_id); + pub(crate) fn begin_layout(&self, constraints: Size) -> Option> { + self.tree.begin_layout(self.node_id, constraints) + } + + pub(crate) fn persist_layout(&self, constraints: Size, size: Size) { + self.tree.persist_layout(self.node_id, constraints, size); } pub(crate) fn visually_ordered_children(&self, order: VisualOrder) -> Vec { @@ -1343,7 +1349,7 @@ impl AsRef for WidgetRef { /// /// Each [`WidgetInstance`] is guaranteed to have a unique [`WidgetId`] across /// the lifetime of an application. -#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)] +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Ord, PartialOrd)] pub struct WidgetId(u64); impl WidgetId { diff --git a/src/widgets/container.rs b/src/widgets/container.rs index 34eb3ce..3b22687 100644 --- a/src/widgets/container.rs +++ b/src/widgets/container.rs @@ -6,7 +6,7 @@ use kludgine::Color; use crate::context::{GraphicsContext, LayoutContext, WidgetContext}; use crate::styles::components::{IntrinsicPadding, SurfaceColor}; -use crate::styles::{Component, ContainerLevel, Dimension, Edges, Styles}; +use crate::styles::{Component, ContainerLevel, Dimension, Edges, RequireInvalidation, Styles}; use crate::value::{IntoValue, Value}; use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; use crate::ConstraintLimit; @@ -250,6 +250,12 @@ impl From for Component { } } +impl RequireInvalidation for EffectiveBackground { + fn requires_invalidation(&self) -> bool { + false + } +} + define_components! { Container { /// The container background behind the current widget. diff --git a/src/widgets/input.rs b/src/widgets/input.rs index e15ec30..4b74396 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -85,7 +85,7 @@ impl Input { context.get(&LineHeight).into_px(scale).into_float(), ), ); - self.text.map(|text| { + self.text.map_tracking_invalidate(context, |text| { buffer.set_text( kludgine.font_system(), text, diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 266c9cd..0a573f0 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -7,7 +7,7 @@ use kludgine::Color; use crate::context::{GraphicsContext, LayoutContext}; use crate::styles::components::{IntrinsicPadding, TextColor}; -use crate::value::{Dynamic, IntoValue, Value}; +use crate::value::{Dynamic, Generation, IntoValue, Value}; use crate::widget::{MakeWidget, Widget, WidgetInstance}; use crate::ConstraintLimit; @@ -16,7 +16,7 @@ use crate::ConstraintLimit; pub struct Label { /// The contents of the label. pub text: Value, - prepared_text: Option<(MeasuredText, Px, Color)>, + prepared_text: Option<(MeasuredText, Option, Px, Color)>, } impl Label { @@ -34,29 +34,34 @@ impl Label { color: Color, width: Px, ) -> &MeasuredText { + let check_generation = self.text.generation(); match &self.prepared_text { - Some((_, prepared_width, prepared_color)) - if *prepared_color == color && *prepared_width == width => {} + Some((prepared, prepared_generation, prepared_width, prepared_color)) + if *prepared_generation == check_generation + && *prepared_color == color + && (*prepared_width == width + || (*prepared_width < width + && prepared.line_height == prepared.size.height)) => {} _ => { let measured = self.text.map(|text| { context .gfx .measure_text(Text::new(text, color).wrap_at(width)) }); - self.prepared_text = Some((measured, width, color)); + self.prepared_text = Some((measured, check_generation, width, color)); } } self.prepared_text .as_ref() - .map(|(prepared, _, _)| prepared) + .map(|(prepared, _, _, _)| prepared) .expect("always initialized") } } impl Widget for Label { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - self.text.redraw_when_changed(context); + self.text.invalidate_when_changed(context); let size = context.gfx.region().size; let center = Point::from(size) / 2; diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index bbc076d..6dd210c 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -176,7 +176,7 @@ where let half_knob = knob_size / 2; - let mut value = self.value.get_tracked(context); + let mut value = self.value.get_tracking_refresh(context); let min = self.minimum.get_tracked(context); let mut max = self.maximum.get_tracked(context); diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index 118cdb4..4eb0172 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -402,15 +402,17 @@ impl Layout { let mut remaining = available_space.saturating_sub(allocated_space); // Measure the children that fit their content + self.other = UPx(0); for &id in &self.measured { let index = self.children.index_of_id(id).expect("child not found"); - let (measured, _) = self.orientation.split_size(measure( + let (measured, other) = self.orientation.split_size(measure( index, self.orientation .make_size(ConstraintLimit::ClippedAfter(remaining), other_constraint), false, )); self.layouts[index].size = measured; + self.other = self.other.max(other); remaining = remaining.saturating_sub(measured); } @@ -435,24 +437,23 @@ impl Layout { self.layouts[index].size = size; } - } - // Now that we know the constrained sizes, we can measure the children - // to get the other measurement using the constrainted measurement. - self.other = UPx(0); - let mut offset = UPx(0); - for index in 0..self.children.len() { - self.layouts[index].offset = offset; - offset += self.layouts[index].size; - let (_, measured) = self.orientation.split_size(measure( - index, - self.orientation.make_size( - ConstraintLimit::Known(self.layouts[index].size.into_px(scale).into_unsigned()), - other_constraint, - ), - false, - )); - self.other = self.other.max(measured); + // Now that we know the constrained sizes, we can measure the children + // to get the other measurement using the constrainted measurement. + for (id, _) in &self.fractional { + let index = self.children.index_of_id(*id).expect("child not found"); + let (_, measured) = self.orientation.split_size(measure( + index, + self.orientation.make_size( + ConstraintLimit::Known( + self.layouts[index].size.into_px(scale).into_unsigned(), + ), + other_constraint, + ), + true, + )); + self.other = self.other.max(measured); + } } self.other = match other_constraint { @@ -461,7 +462,10 @@ impl Layout { }; // Finally layout the widgets with the final constraints + let mut offset = UPx(0); for index in 0..self.children.len() { + self.layouts[index].offset = offset; + offset += self.layouts[index].size; self.orientation.split_size(measure( index, self.orientation.make_size( diff --git a/src/window.rs b/src/window.rs index f14c46e..76c9b5c 100644 --- a/src/window.rs +++ b/src/window.rs @@ -27,7 +27,7 @@ use tracing::Level; use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne}; use crate::context::{ - AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, RedrawStatus, + AsEventContext, EventContext, Exclusive, GraphicsContext, InvalidationStatus, LayoutContext, WidgetContext, }; use crate::graphics::Graphics; @@ -275,7 +275,7 @@ struct GooeyWindow { contents: Drawing, should_close: bool, mouse_state: MouseState, - redraw_status: RedrawStatus, + redraw_status: InvalidationStatus, initial_frame: bool, occluded: Dynamic, focused: Dynamic, @@ -467,7 +467,7 @@ where widget: None, devices: AHashMap::default(), }, - redraw_status: RedrawStatus::default(), + redraw_status: InvalidationStatus::default(), initial_frame: true, occluded, focused, @@ -497,7 +497,9 @@ where self.redraw_status.refresh_received(); graphics.reset_text_attributes(); - self.root.tree.reset_render_order(); + // TODO re-check why we can't add drain without a range to kempt. Or even intoiter. + let invalidations = std::mem::take(&mut *self.redraw_status.invalidations()); + self.root.tree.new_frame(invalidations.iter().copied()); let resizable = window.winit().is_resizable(); let is_expanded = self.constrain_window_resizing(resizable, &window, graphics); @@ -651,7 +653,13 @@ where // fn scale_factor_changed(&mut self, window: kludgine::app::Window<'_, ()>) {} - // fn resized(&mut self, window: kludgine::app::Window<'_, ()>) {} + fn resized( + &mut self, + _window: kludgine::app::Window<'_, WindowCommand>, + _kludgine: &mut Kludgine, + ) { + self.root.invalidate(); + } // fn theme_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}