Layout caching, Lerp underflow fix, label fix

This commit is contained in:
Jonathan Johnson 2023-11-14 07:36:22 -08:00
parent cc7d4bac45
commit a04619a279
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
16 changed files with 440 additions and 184 deletions

10
Cargo.lock generated
View file

@ -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",
]

View file

@ -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)

View file

@ -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<Animating> = 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<T: LinearInterpolate + Debug + Eq>(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<Component> 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

View file

@ -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<ConstraintLimit>) -> Size<UPx> {
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<T>(&self, value: &Dynamic<T>) {
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<Rect<Px>> {
@ -963,7 +980,24 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
pub(crate) struct WindowHandle {
kludgine: kludgine::app::WindowHandle<WindowCommand>,
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<H: std::hash::Hasher>(&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<AtomicBool>,
invalidated: Arc<Mutex<Set<WidgetId>>>,
}
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<WidgetId>> {
self.invalidated.lock().ignore_poison()
}
}
/// A type chat can convert to a [`ManagedWidget`] through a [`WidgetContext`].

View file

@ -86,8 +86,17 @@ impl Styles {
self.0
.get(&name)
.and_then(|component| {
component.redraw_when_changed(context);
<Named::ComponentType>::try_from_component(component.get()).ok()
match <Named::ComponentType>::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<Component> for Color {
}
}
impl RequireInvalidation for Color {
fn requires_invalidation(&self) -> bool {
false
}
}
impl From<Dimension> for Component {
fn from(value: Dimension) -> Self {
Self::Dimension(value)
@ -236,6 +251,12 @@ impl TryFrom<Component> for Dimension {
}
}
impl RequireInvalidation for Dimension {
fn requires_invalidation(&self) -> bool {
true
}
}
impl From<Px> for Component {
fn from(value: Px) -> Self {
Self::from(Dimension::from(value))
@ -253,6 +274,12 @@ impl TryFrom<Component> for Px {
}
}
impl RequireInvalidation for Px {
fn requires_invalidation(&self) -> bool {
true
}
}
impl From<Lp> for Component {
fn from(value: Lp) -> Self {
Self::from(Dimension::from(value))
@ -270,6 +297,12 @@ impl TryFrom<Component> 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<Component> for DimensionRange {
}
}
impl RequireInvalidation for DimensionRange {
fn requires_invalidation(&self) -> bool {
true
}
}
/// A custom component value.
#[derive(Debug, Clone)]
pub struct CustomComponent(Arc<dyn AnyComponent>);
@ -571,7 +610,7 @@ impl CustomComponent {
/// Wraps an arbitrary value so that it can be used as a [`Component`].
pub fn new<T>(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<T> 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<T> ComponentType for T
where
T: Into<Component> + TryFrom<Component, Error = Component>,
T: RequireInvalidation + Into<Component> + TryFrom<Component, Error = Component>,
{
fn into_component(self) -> Component {
self.into()
@ -1411,6 +1468,12 @@ impl TryFrom<Component> 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<Component> 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<Component> for ContainerLevel {
}
}
impl RequireInvalidation for ContainerLevel {
fn requires_invalidation(&self) -> bool {
true
}
}
/// A builder of [`ColorScheme`]s.
#[derive(Clone, Copy, Debug)]
pub struct ColorSchemeBuilder {

View file

@ -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;
}

View file

@ -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<Px>) {
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<Rect<Px>> {
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<Item = WidgetId>) {
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<ConstraintLimit>,
) -> Option<Size<UPx>> {
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<ConstraintLimit>,
size: Size<UPx>,
) {
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<ManagedWidget> {
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::<ManagedWidget>::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<Option<ManagedWidget>, ()> {
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<Option<ManagedWidget>, ()> {
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<ManagedWidget> {
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<ManagedWidget> {
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<LotId> {
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<LotId> {
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<LotId> {
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<LotId> {
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<LotId> {
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<Px>) -> Vec<ManagedWidget> {
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<LotId> {
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<Styles>) {
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<ThemePair>) {
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<ThemeMode>) {
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<Value<ThemePair>>, Option<Value<ThemeMode>>) {
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<LotId>,
pub parent: Option<LotId>,
pub layout: Option<Rect<Px>>,
pub associated_styles: Option<Value<Styles>>,
pub effective_styles: Styles,
pub theme: Option<Value<ThemePair>>,
pub theme_mode: Option<Value<ThemeMode>>,
struct Node {
widget: WidgetInstance,
children: Vec<LotId>,
parent: Option<LotId>,
layout: Option<Rect<Px>>,
last_layout_query: Option<CachedLayoutQuery>,
associated_styles: Option<Value<Styles>>,
effective_styles: Styles,
theme: Option<Value<ThemePair>>,
theme_mode: Option<Value<ThemeMode>>,
}
impl Node {
@ -475,3 +516,8 @@ impl Node {
effective_styles
}
}
struct CachedLayoutQuery {
constraints: Size<ConstraintLimit>,
size: Size<UPx>,
}

View file

@ -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<T> Deref for Lazy<T> {
self.once.get_or_init(self.init)
}
}
pub trait IgnorePoison {
type Unwrapped;
fn ignore_poison(self) -> Self::Unwrapped;
}
impl<T> IgnorePoison for Result<T, PoisonError<T>> {
type Unwrapped = T;
fn ignore_poison(self) -> Self::Unwrapped {
self.map_or_else(PoisonError::into_inner, |g| g)
}
}

View file

@ -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<T> Dynamic<T> {
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<T> Dynamic<T> {
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<T> Dynamic<T> {
/// 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<T> Dynamic<T> {
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<T> {
impl<T> DynamicData<T> {
fn state(&self) -> Result<DynamicMutexGuard<'_, T>, 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<T> DynamicData<T> {
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<T> DynamicData<T> {
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<GenerationalValue<T>, DeadlockError>
@ -579,7 +598,8 @@ impl Display for DeadlockError {
struct State<T> {
wrapped: GenerationalValue<T>,
callbacks: Vec<Box<dyn ValueCallback<T>>>,
windows: Vec<WindowHandle>,
windows: AHashSet<WindowHandle>,
widgets: AHashSet<(WindowHandle, WidgetId)>,
wakers: Vec<Waker>,
readers: usize,
}
@ -591,7 +611,10 @@ impl<T> State<T> {
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<T> DynamicReader<T> {
/// 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<T> DynamicReader<T> {
"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<T> DynamicReader<T> {
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<T> Value<T> {
///
/// If `self` is a dynamic, `context` will be invalidated when the value is
/// updated.
pub fn map_tracked<R>(&self, context: &WidgetContext<'_, '_>, map: impl FnOnce(&T) -> R) -> R {
pub fn map_tracking_redraw<R>(
&self,
context: &WidgetContext<'_, '_>,
map: impl FnOnce(&T) -> R,
) -> R {
match self {
Value::Constant(value) => map(value),
Value::Dynamic(dynamic) => {
@ -946,6 +961,24 @@ impl<T> Value<T> {
}
}
/// 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<R>(
&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<R>(&mut self, map: impl FnOnce(&mut T) -> R) -> R {
match self {
@ -984,7 +1017,7 @@ impl<T> Value<T> {
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<T> Value<T> {
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<T> Clone for Value<T>
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,)+));
}
}));

View file

@ -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<Px>) {
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<ConstraintLimit>) -> Option<Size<UPx>> {
self.tree.begin_layout(self.node_id, constraints)
}
pub(crate) fn persist_layout(&self, constraints: Size<ConstraintLimit>, size: Size<UPx>) {
self.tree.persist_layout(self.node_id, constraints, size);
}
pub(crate) fn visually_ordered_children(&self, order: VisualOrder) -> Vec<ManagedWidget> {
@ -1343,7 +1349,7 @@ impl AsRef<WidgetId> 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 {

View file

@ -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<EffectiveBackground> for Component {
}
}
impl RequireInvalidation for EffectiveBackground {
fn requires_invalidation(&self) -> bool {
false
}
}
define_components! {
Container {
/// The container background behind the current widget.

View file

@ -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,

View file

@ -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<String>,
prepared_text: Option<(MeasuredText<Px>, Px, Color)>,
prepared_text: Option<(MeasuredText<Px>, Option<Generation>, Px, Color)>,
}
impl Label {
@ -34,29 +34,34 @@ impl Label {
color: Color,
width: Px,
) -> &MeasuredText<Px> {
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;

View file

@ -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);

View file

@ -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(

View file

@ -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<T> {
contents: Drawing,
should_close: bool,
mouse_state: MouseState,
redraw_status: RedrawStatus,
redraw_status: InvalidationStatus,
initial_frame: bool,
occluded: Dynamic<bool>,
focused: Dynamic<bool>,
@ -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<'_, ()>) {}