mirror of
https://github.com/danbulant/cushy
synced 2026-05-24 12:28:23 +00:00
Layout caching, Lerp underflow fix, label fix
This commit is contained in:
parent
cc7d4bac45
commit
a04619a279
16 changed files with 440 additions and 184 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`].
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
12
src/tick.rs
12
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;
|
||||
}
|
||||
|
|
|
|||
170
src/tree.rs
170
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<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>,
|
||||
}
|
||||
|
|
|
|||
15
src/utils.rs
15
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<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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
118
src/value.rs
118
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<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,)+));
|
||||
}
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<'_, ()>) {}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue