WindowLocal + Custom Observers

This cascaded into a lot more work than expected. However, in general,
if one clones a `WidgetInstance` and shares it between two windows, it
should now work. Widget authors must ensure that when they cache
information, they do so with either a `WidgetCacheKey` or use a
`WindowLocal<T>` if per-window state is desired.

This is demonstrated in the debug-window example, where the counter of
open windows is next to a clone of the same button from the main window
that opens a new window.
This commit is contained in:
Jonathan Johnson 2023-12-29 13:21:39 -08:00
parent 999f920f8c
commit 9e4e079bf5
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
16 changed files with 309 additions and 133 deletions

View file

@ -7,12 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Breaking Changes
- `WidgetRef` is now a `struct` instead of an enum. This refactor changes the
mounted state to be stored in a `WindowLocal`, ensuring `WidgetRef`s work
properly when used in a `WidgetInstance` shared between multiple windows.
- `WidgetRef::unmount_in` should be called when the widget is being unmounted to
clean up individual window state.
### Fixed
- The root widget is now included in the search for widgets to accept focus.
- Widgets that have been laid out with a 0px width or height no longer have
their `redraw` functions called nor can they receive focus.
- `Grid` now synchronizes removal of widgets from `GridWidgets` correctly.
- `WidgetInstance`s can now be shared between windows. Any unpredictable
behaviors when doing this should be reported, as some widgets may still have
state that should be moved into a `WindowLocal` type.
- `Grid` no longer passes `ConstraintLimit::Fill` along to children when it
contains more than one element. Previously, if rows contained widgets that
filled the given space, this would cause the grid to calculate layouts
incorrectly.
### Changed
- `WidgetCacheKey` now includes the `KludgineId` of the context it was created
from. This ensures if a `WidgetInstance` moves or is shared between windows,
the cache is invalidated.
### Added
@ -43,6 +64,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
existence.
- `Dynamic::readers()` returns the number of `DynamicReader`s for the dynamic in
existence.
- `RunningWindow::kludgine_id()` returns a unique id for that window.
- `WindowLocal<T>` is a `HashMap`-based type that stores data on a per-window
basis using `RunningWindow::kludgine_id()` as the key.
[99]: https://github.com/khonsulabs/cushy/issues/99

10
Cargo.lock generated
View file

@ -35,9 +35,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.8.6"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
dependencies = [
"cfg-if",
"getrandom",
@ -1155,9 +1155,9 @@ checksum = "d7e253b574775d0ebd7975c471fc18f72f0775a4d42b563b5fbc3c4068aa1075"
[[package]]
name = "kempt"
version = "0.2.2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13a912e97bbe1ec7cbead29896008766dedc4790350c55aece45998fde067200"
checksum = "4a37a6bdb52eae6acb1efbab069af766555a6d77712380fd48876b83543d3071"
[[package]]
name = "khronos-egl"
@ -1179,7 +1179,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kludgine"
version = "0.7.0"
source = "git+https://github.com/khonsulabs/kludgine#a38466b021ce55c26c6d533191a87a22109016b2"
source = "git+https://github.com/khonsulabs/kludgine#0bda2a6dc273aa49338f23ea5190aefdf037d740"
dependencies = [
"ahash",
"alot",

View file

@ -8,20 +8,32 @@ const INTRO: &str = "This example demonstrates the DebugContext, which allows ob
fn main() -> cushy::Result {
let app = PendingApp::default();
let dbg = DebugContext::default();
let window_count = Dynamic::new(0_usize);
let total_windows = Dynamic::new(0_usize);
let info = DebugContext::default();
dbg.observe("Open Windows", &window_count);
dbg.observe("Total Windows", &total_windows);
dbg.clone().open(&app)?;
let window_count = Dynamic::new(0_usize);
let total_windows = info.dbg("Total Windows", Dynamic::new(0_usize));
let open_window_button = "Open a Window"
.into_button()
.on_click({
let app = app.as_app();
let info = info.clone();
let window_count = window_count.clone();
let total_windows = total_windows.clone();
move |()| open_a_window(&window_count, &total_windows, &info, &app)
})
.make_widget();
info.observe("Open Windows", &window_count, |window_count| {
window_count
.map_each(ToString::to_string)
.and(open_window_button.clone())
.into_columns()
});
info.clone().open(&app)?;
INTRO
.and("Open a Window".into_button().on_click({
let app = app.as_app();
move |()| open_a_window(&window_count, &total_windows, &dbg, &app)
}))
.and(open_window_button)
.into_rows()
.centered()
.run_in(app)
@ -30,7 +42,7 @@ fn main() -> cushy::Result {
fn open_a_window(
window_count: &Dynamic<usize>,
total_windows: &Dynamic<usize>,
dbg: &DebugContext,
info: &DebugContext,
app: &dyn Application,
) {
*window_count.lock() += 1;
@ -39,10 +51,9 @@ fn open_a_window(
*total
});
let window_title = format!("Window #{window_number}");
let dbg = dbg.section(&window_title);
let dbg = info.section(&window_title);
let value = Dynamic::new(0_u8);
dbg.observe("Slider", &value);
let value = dbg.dbg("Slider", Dynamic::new(0_u8));
let window_count = window_count.clone();
let _ = format!("This is window {window_number}.")

View file

@ -12,7 +12,7 @@ use kludgine::app::winit::event::{
};
use kludgine::app::winit::window::CursorIcon;
use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::{Color, Kludgine};
use kludgine::{Color, Kludgine, KludgineId};
use crate::animation::ZeroToOne;
use crate::graphics::Graphics;
@ -24,9 +24,7 @@ use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair};
use crate::tree::Tree;
use crate::utils::IgnorePoison;
use crate::value::{IntoValue, Value};
use crate::widget::{
EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance, WidgetRef,
};
use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance};
use crate::window::{CursorState, RunningWindow, ThemeMode};
use crate::ConstraintLimit;
@ -893,6 +891,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
tree,
effective_styles: current_node.effective_styles(),
cache: WidgetCacheKey {
kludgine_id: Some(window.kludgine_id()),
theme_mode,
enabled,
},
@ -941,6 +940,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
WidgetContext {
effective_styles,
cache: WidgetCacheKey {
kludgine_id: self.cache.kludgine_id,
theme_mode,
enabled: current_node.enabled(&self.handle()),
},
@ -1333,17 +1333,6 @@ impl ManageWidget for WidgetInstance {
}
}
impl ManageWidget for WidgetRef {
type Managed = Option<MountedWidget>;
fn manage(&self, context: &WidgetContext<'_, '_>) -> Self::Managed {
match self {
WidgetRef::Unmounted(instance) => context.tree.widget(instance.id()),
WidgetRef::Mounted(instance) => Some(instance.clone()),
}
}
}
impl ManageWidget for MountedWidget {
type Managed = Self;
@ -1383,6 +1372,7 @@ impl<T> MapManagedWidget<T> for MountedWidget {
/// keys are not equal, the widget should clear all caches.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct WidgetCacheKey {
kludgine_id: Option<KludgineId>,
theme_mode: ThemeMode,
enabled: bool,
}
@ -1390,6 +1380,7 @@ pub struct WidgetCacheKey {
impl Default for WidgetCacheKey {
fn default() -> Self {
Self {
kludgine_id: None,
theme_mode: ThemeMode::default().inverse(),
enabled: false,
}

View file

@ -17,23 +17,43 @@ pub struct DebugContext {
}
impl DebugContext {
/// Observes `value` using `label` in this debug context.
/// Observes `value` by showing the `Debug` output. Returns `value`.
///
/// When the final reference to `value` is dropped, this observation will
/// automatically be removed.
pub fn observe<T>(&self, label: impl Into<String>, value: &Dynamic<T>)
///
/// This function is designed to work similarly to the [`dbg!`] macro.
pub fn dbg<T>(&self, label: impl Into<String>, value: Dynamic<T>) -> Dynamic<T>
where
T: PartialEq + Clone + Debug + Send + Sync + 'static,
T: Clone + Debug + Send + Sync + 'static,
{
self.observe(label, &value, |value| {
value.map_each(|value| format!("{value:?}")).make_widget()
});
value
}
/// Observes `value` by attaching the widget created by `make_observer` to
/// this context.
///
/// When the final reference to `value` is dropped, this observation will
/// automatically be removed.
pub fn observe<T, Widget, MakeObserver>(
&self,
label: impl Into<String>,
value: &Dynamic<T>,
make_observer: MakeObserver,
) where
T: Clone + Send + Sync + 'static,
MakeObserver: FnOnce(Dynamic<T>) -> Widget,
Widget: MakeWidget,
{
let reader = value.create_reader();
let id = self.section.map_ref(|section| {
section.values.lock().push(Box::new(RegisteredValue {
label: label.into(),
value: reader.clone(),
widget: value
.weak_clone()
.map_each(|value| format!("{value:?}"))
.make_widget(),
widget: make_observer(value.weak_clone()).make_widget(),
}))
});
let this = self.clone();
@ -79,7 +99,7 @@ impl DebugContext {
fn into_window(self) -> Window {
self.section
.map_ref(|section| section.widget.clone())
.vertical_scroll()
// .vertical_scroll()
.into_window()
.titled("Cushy Debugger")
}
@ -173,7 +193,7 @@ impl DebugSection {
let value_grid = Grid::from_rows(values.map_each(|values| {
values
.iter()
.map(|o| [o.label().make_widget(), o.widget().clone()])
.map(|o| (o.label(), o.widget().clone().align_left()))
.collect::<GridWidgets<2>>()
}));

View file

@ -83,15 +83,17 @@ impl Tree {
let node = &mut data.nodes[widget];
node.layout = Some(rect);
let mut children_to_offset = node.children.clone();
while let Some(child) = children_to_offset.pop() {
if let Some(layout) = data
.nodes
.get_mut(child)
.and_then(|child| child.layout.as_mut())
{
layout.origin += rect.origin;
children_to_offset.extend(data.nodes[child].children.iter().copied());
if !node.children.is_empty() {
let mut children_to_offset = node.children.clone();
while let Some(child) = children_to_offset.pop() {
if let Some(layout) = data
.nodes
.get_mut(child)
.and_then(|child| child.layout.as_mut())
{
layout.origin += rect.origin;
children_to_offset.extend(data.nodes[child].children.iter().copied());
}
}
}
}
@ -399,6 +401,20 @@ impl Tree {
}
}
impl Eq for Tree {}
impl PartialEq for Tree {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.data, &other.data)
}
}
impl PartialEq<WeakTree> for Tree {
fn eq(&self, other: &WeakTree) -> bool {
Arc::as_ptr(&self.data) == Weak::as_ptr(&other.0)
}
}
pub(crate) struct HoverResults {
pub unhovered: Vec<MountedWidget>,
pub hovered: Vec<MountedWidget>,

View file

@ -268,7 +268,7 @@ impl<T> Dynamic<T> {
#[must_use]
pub fn weak_clone(&self) -> Self
where
T: Clone + PartialEq + Send + 'static,
T: Clone + Send + 'static,
{
let weak_source = self.downgrade();
let weak_out = Dynamic::new(self.get());
@ -276,7 +276,7 @@ impl<T> Dynamic<T> {
let weak_out = weak_out.clone();
move || {
if let Some(source) = weak_source.upgrade() {
weak_out.set(source.get());
*weak_out.lock() = source.get();
}
}
}));

View file

@ -19,7 +19,9 @@ use kludgine::app::winit::window::CursorIcon;
use kludgine::Color;
use crate::app::{Application, Open, PendingApp, Run};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext};
use crate::context::{
AsEventContext, EventContext, GraphicsContext, LayoutContext, ManageWidget, WidgetContext,
};
use crate::styles::components::{
FontFamily, FontStyle, FontWeight, Heading1FontFamily, Heading1Style, Heading1Weight,
Heading2FontFamily, Heading2Style, Heading2Weight, Heading3FontFamily, Heading3Style,
@ -42,7 +44,7 @@ use crate::widgets::{
Align, Button, Checkbox, Collapse, Container, Disclose, Expand, Layers, Resize, Scroll, Space,
Stack, Style, Themed, ThemedMode, Validated, Wrap,
};
use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior, WindowHandle};
use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior, WindowHandle, WindowLocal};
use crate::ConstraintLimit;
/// A type that makes up a graphical user interface.
@ -620,7 +622,9 @@ pub trait WrapperWidget: Debug + Send + 'static {
/// The widget has been removed from its parent widget.
#[allow(unused_variables)]
fn unmounted(&mut self, context: &mut EventContext<'_, '_>) {}
fn unmounted(&mut self, context: &mut EventContext<'_, '_>) {
self.child_mut().unmount_in(context);
}
/// Returns true if this widget should respond to mouse input at `location`.
#[allow(unused_variables)]
@ -2237,24 +2241,34 @@ impl MountableChild for MountedWidget {
/// A child widget
#[derive(Clone)]
pub enum WidgetRef {
/// An unmounted child widget
Unmounted(WidgetInstance),
/// A mounted child widget
Mounted(MountedWidget),
pub struct WidgetRef {
instance: WidgetInstance,
mounted: WindowLocal<MountedWidget>,
}
impl WidgetRef {
/// Returns a new unmounted child
pub fn new(widget: impl MakeWidget) -> Self {
Self::Unmounted(widget.make_widget())
Self {
instance: widget.make_widget(),
mounted: WindowLocal::default(),
}
}
/// Returns this child, mounting it in the process if necessary.
fn mounted_for_context<'window>(
&mut self,
context: &mut impl AsEventContext<'window>,
) -> &MountedWidget {
let mut context = context.as_event_context();
self.mounted
.entry(&context)
.or_insert_with(|| context.push_child(self.instance.clone()))
}
/// Returns this child, mounting it in the process if necessary.
pub fn mount_if_needed<'window>(&mut self, context: &mut impl AsEventContext<'window>) {
if let WidgetRef::Unmounted(instance) = self {
*self = WidgetRef::Mounted(context.push_child(instance.clone()));
}
self.mounted_for_context(context);
}
/// Returns this child, mounting it in the process if necessary.
@ -2262,39 +2276,39 @@ impl WidgetRef {
&mut self,
context: &mut impl AsEventContext<'window>,
) -> MountedWidget {
self.mount_if_needed(context);
self.mounted_for_context(context).clone()
}
let Self::Mounted(widget) = self else {
unreachable!("just initialized")
};
widget.clone()
/// Returns this child, mounting it in the process if necessary.
#[must_use]
pub fn as_mounted(&self, context: &WidgetContext<'_, '_>) -> Option<&MountedWidget> {
self.mounted.get(context)
}
/// Returns the a reference to the underlying widget instance.
#[must_use]
pub fn widget(&self) -> &WidgetInstance {
match self {
WidgetRef::Unmounted(widget) => widget,
WidgetRef::Mounted(managed) => &managed.widget,
pub const fn widget(&self) -> &WidgetInstance {
&self.instance
}
/// Unmounts this widget from the window belonging to `context`, if needed.
pub fn unmount_in<'window>(&mut self, context: &mut impl AsEventContext<'window>) {
let mut context = context.as_event_context();
if let Some(mounted) = self.mounted.clear_for(&context) {
context.remove_child(&mounted);
}
}
}
impl AsRef<WidgetId> for WidgetRef {
fn as_ref(&self) -> &WidgetId {
match self {
WidgetRef::Unmounted(widget) => widget.as_ref(),
WidgetRef::Mounted(widget) => widget.as_ref(),
}
self.instance.as_ref()
}
}
impl Debug for WidgetRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unmounted(arg0) => Debug::fmt(arg0, f),
Self::Mounted(arg0) => Debug::fmt(arg0, f),
}
Debug::fmt(&self.instance, f)
}
}
@ -2302,11 +2316,18 @@ impl Eq for WidgetRef {}
impl PartialEq for WidgetRef {
fn eq(&self, other: &Self) -> bool {
if let (WidgetRef::Mounted(this), WidgetRef::Mounted(other)) = (self, other) {
this == other
} else {
self.widget() == other.widget()
}
self.instance == other.instance
}
}
impl ManageWidget for WidgetRef {
type Managed = Option<MountedWidget>;
fn manage(&self, context: &WidgetContext<'_, '_>) -> Self::Managed {
self.mounted
.get(context)
.cloned()
.or_else(|| context.tree.widget(self.instance.id()))
}
}

View file

@ -21,6 +21,7 @@ use crate::styles::components::{
use crate::styles::{ColorExt, Styles};
use crate::value::{Dynamic, IntoValue, Value};
use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED};
use crate::window::WindowLocal;
use crate::FitMeasuredSize;
/// A clickable button.
@ -33,13 +34,18 @@ pub struct Button {
/// The kind of button to draw.
pub kind: Value<ButtonKind>,
focusable: bool,
per_window: WindowLocal<PerWindow>,
}
#[derive(Debug, Default)]
struct PerWindow {
buttons_pressed: usize,
cached_state: CacheState,
active_colors: Option<Dynamic<ButtonColors>>,
color_animation: AnimationHandle,
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
#[derive(Default, Debug, Eq, PartialEq, Clone, Copy)]
struct CacheState {
key: WidgetCacheKey,
kind: ButtonKind,
@ -133,14 +139,8 @@ impl Button {
Self {
content: content.widget_ref(),
on_click: None,
cached_state: CacheState {
key: WidgetCacheKey::default(),
kind: ButtonKind::default(),
},
buttons_pressed: 0,
active_colors: None,
per_window: WindowLocal::default(),
kind: Value::Constant(ButtonKind::default()),
color_animation: AnimationHandle::default(),
focusable: true,
}
}
@ -229,7 +229,9 @@ impl Button {
let kind = self.kind.get_tracking_redraw(context);
let visual_state = Self::visual_style(context);
self.cached_state = CacheState {
let window_local = self.per_window.entry(context).or_default();
window_local.cached_state = CacheState {
key: context.cache_key(),
kind,
};
@ -247,33 +249,46 @@ impl Button {
fn update_colors(&mut self, context: &mut WidgetContext<'_, '_>, immediate: bool) {
let new_style = self.determine_stateful_colors(context);
let window_local = self.per_window.entry(context).or_default();
match (immediate, &self.active_colors) {
match (immediate, &window_local.active_colors) {
(false, Some(style)) => {
self.color_animation = (style.transition_to(new_style))
window_local.color_animation = (style.transition_to(new_style))
.over(Duration::from_millis(150))
.with_easing(context.get(&Easing))
.spawn();
}
(true, Some(style)) => {
style.set(new_style);
self.color_animation.clear();
window_local.color_animation.clear();
}
_ => {
let new_style = Dynamic::new(new_style);
let foreground = new_style.map_each(|s| s.foreground);
self.active_colors = Some(new_style);
window_local.active_colors = Some(new_style);
context.attach_styles(Styles::new().with(&TextColor, foreground));
}
}
}
fn current_style(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors {
if self.active_colors.is_none() {
if self
.per_window
.entry(context)
.or_default()
.active_colors
.is_none()
{
self.update_colors(context, false);
}
let style = self.active_colors.as_ref().expect("always initialized");
let style = self
.per_window
.entry(context)
.or_default()
.active_colors
.as_ref()
.expect("always initialized");
context.redraw_when_changed(style);
style.get()
}
@ -353,7 +368,10 @@ impl Widget for Button {
#![allow(clippy::similar_names)]
let current_style = self.kind.get_tracking_redraw(context);
if self.cached_state.key != context.cache_key() || self.cached_state.kind != current_style {
let window_local = self.per_window.entry(context).or_default();
if window_local.cached_state.key != context.cache_key()
|| window_local.cached_state.kind != current_style
{
self.update_colors(context, false);
}
@ -414,7 +432,7 @@ impl Widget for Button {
_button: MouseButton,
context: &mut EventContext<'_, '_>,
) -> EventHandling {
self.buttons_pressed += 1;
self.per_window.entry(context).or_default().buttons_pressed += 1;
context.activate();
HANDLED
}
@ -446,8 +464,9 @@ impl Widget for Button {
_button: MouseButton,
context: &mut EventContext<'_, '_>,
) {
self.buttons_pressed -= 1;
if self.buttons_pressed == 0 {
let window_local = self.per_window.entry(context).or_default();
window_local.buttons_pressed -= 1;
if window_local.buttons_pressed == 0 {
context.deactivate();
if let (true, Some(location)) = (self.focusable, location) {
@ -472,7 +491,7 @@ impl Widget for Button {
.into_upx(context.gfx.scale())
.round();
let double_padding = padding * 2;
let mounted = self.content.mounted(&mut context.as_event_context());
let mounted = self.content.mounted(context);
let available_space = available_space.map(|space| space - double_padding);
let size = context.for_other(&mounted).layout(available_space);
let size = available_space.fit_measured(size, context.gfx.scale());
@ -510,9 +529,10 @@ impl Widget for Button {
}
fn activate(&mut self, context: &mut EventContext<'_, '_>) {
let window_local = self.per_window.entry(context).or_default();
// If we have no buttons pressed, the event should fire on activate not
// on deactivate.
if self.buttons_pressed == 0 {
if window_local.buttons_pressed == 0 {
self.invoke_on_click(context);
}
self.update_colors(context, true);

View file

@ -110,7 +110,7 @@ impl DiscloseIndicator {
contents: WidgetRef::new(contents.collapse_vertically(collapsed.clone())),
collapsed,
hovering_indicator: false,
label: label.map(WidgetRef::Unmounted),
label: label.map(WidgetRef::new),
target_colors: None,
color: Dynamic::new(Color::CLEAR_WHITE),
stroke_color: Dynamic::new(Color::CLEAR_WHITE),

View file

@ -381,7 +381,7 @@ impl GridLayout {
scale: Fraction,
mut measure: impl FnMut(usize, usize, Size<ConstraintLimit>, bool) -> Size<UPx>,
) -> Size<UPx> {
let (space_constraint, other_constraint) = self.orientation.split_size(available);
let (space_constraint, mut other_constraint) = self.orientation.split_size(available);
let available_space = space_constraint.max();
let known_gutters = gutter.saturating_mul(UPx::new(
(self.children.len() - self.fit_to_content.len())
@ -391,6 +391,13 @@ impl GridLayout {
let allocated_space =
self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil() + known_gutters;
let mut remaining = available_space.saturating_sub(allocated_space);
if self.elements_per_child > 1 {
// When we are in multi-row mode, we force a size-to-fit mode for
// children. Trying to ask each row to fill will never work.
other_constraint = ConstraintLimit::SizeToFit(other_constraint.max());
}
// If our `other_constraint` is not known, we will need to give child
// widgets an opportunity to lay themselves out in the full area. This
// requires one extra layout call, so we avoid persisting layouts during

View file

@ -9,6 +9,7 @@ use crate::context::{GraphicsContext, LayoutContext};
use crate::styles::components::TextColor;
use crate::value::{Dynamic, Generation, IntoValue, Value};
use crate::widget::{Widget, WidgetInstance};
use crate::window::WindowLocal;
use crate::ConstraintLimit;
/// A read-only text widget.
@ -16,7 +17,7 @@ use crate::ConstraintLimit;
pub struct Label {
/// The contents of the label.
pub text: Value<String>,
prepared_text: Option<(MeasuredText<Px>, Option<Generation>, Px, Color)>,
prepared_text: WindowLocal<(MeasuredText<Px>, Option<Generation>, Px, Color)>,
}
impl Label {
@ -24,7 +25,7 @@ impl Label {
pub fn new(text: impl IntoValue<String>) -> Self {
Self {
text: text.into_value(),
prepared_text: None,
prepared_text: WindowLocal::default(),
}
}
@ -35,14 +36,15 @@ impl Label {
width: Px,
) -> &MeasuredText<Px> {
let check_generation = self.text.generation();
match &self.prepared_text {
match self.prepared_text.get(context) {
Some((prepared, prepared_generation, prepared_width, prepared_color))
if prepared.can_render_to(&context.gfx)
&& *prepared_generation == check_generation
&& *prepared_color == color
&& (*prepared_width == width
|| ((*prepared_width < width || prepared.size.width <= width)
&& prepared.line_height == prepared.size.height)) => {}
|| (*prepared_width < width
|| (prepared.size.width <= width
&& prepared.line_height == prepared.size.height))) => {}
_ => {
context.apply_current_font_settings();
let measured = self.text.map(|text| {
@ -50,12 +52,13 @@ impl Label {
.gfx
.measure_text(Text::new(text, color).wrap_at(width))
});
self.prepared_text = Some((measured, check_generation, width, color));
self.prepared_text
.set(context, (measured, check_generation, width, color));
}
}
self.prepared_text
.as_ref()
.get(context)
.map(|(prepared, _, _, _)| prepared)
.expect("always initialized")
}
@ -86,12 +89,16 @@ impl Widget for Label {
let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
let prepared = self.prepared_text(context, color, width);
prepared.size.try_cast().unwrap_or_default()
prepared.size.try_cast().unwrap_or_default().ceil()
}
fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.debug_tuple("Label").field(&self.text).finish()
}
fn unmounted(&mut self, context: &mut crate::context::EventContext<'_, '_>) {
self.prepared_text.clear_for(context);
}
}
macro_rules! impl_make_widget {

View file

@ -178,7 +178,7 @@ impl Widget for OverlayLayer {
let state = self.state.lock();
for child in &state.overlays {
let WidgetRef::Mounted(mounted) = &child.widget else {
let Some(mounted) = child.widget.as_mounted(context) else {
continue;
};

View file

@ -92,10 +92,7 @@ impl Stack {
{
(child, size)
} else {
(
WidgetRef::Unmounted(widget.clone()),
GridDimension::FitContent,
)
(WidgetRef::new(widget.clone()), GridDimension::FitContent)
};
drop(guard);
this.insert(index, widget.mounted(context));

View file

@ -2,7 +2,7 @@ use std::fmt::Debug;
use figures::Size;
use crate::context::{AsEventContext, LayoutContext};
use crate::context::LayoutContext;
use crate::value::{Dynamic, DynamicReader, IntoDynamic};
use crate::widget::{WidgetInstance, WidgetRef, WrapperWidget};
use crate::ConstraintLimit;
@ -55,10 +55,7 @@ impl WrapperWidget for Switcher {
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<ConstraintLimit> {
if self.source.has_updated() {
let removed = std::mem::replace(&mut self.child, WidgetRef::new(self.source.get()));
if let WidgetRef::Mounted(removed) = removed {
context.remove_child(&removed);
}
self.child.unmount_in(context);
}
context.invalidate_when_changed(&self.source);
available_space

View file

@ -1,6 +1,7 @@
//! Types for displaying a [`Widget`] inside of a desktop window.
use std::cell::RefCell;
use std::collections::hash_map;
use std::ffi::OsStr;
use std::hash::Hash;
use std::ops::{Deref, DerefMut, Not};
@ -23,7 +24,7 @@ use kludgine::app::WindowBehavior as _;
use kludgine::cosmic_text::{fontdb, Family, FamilyOwned};
use kludgine::render::Drawing;
use kludgine::wgpu::CompositeAlphaMode;
use kludgine::Kludgine;
use kludgine::{Kludgine, KludgineId};
use tracing::Level;
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
@ -47,6 +48,7 @@ use crate::{initialize_tracing, ConstraintLimit};
/// A currently running Cushy window.
pub struct RunningWindow<'window> {
window: kludgine::app::Window<'window, WindowCommand>,
kludgine_id: KludgineId,
invalidation_status: InvalidationStatus,
cushy: Cushy,
focused: Dynamic<bool>,
@ -57,6 +59,7 @@ pub struct RunningWindow<'window> {
impl<'window> RunningWindow<'window> {
pub(crate) fn new(
window: kludgine::app::Window<'window, WindowCommand>,
kludgine_id: KludgineId,
invalidation_status: &InvalidationStatus,
cushy: &Cushy,
focused: &Dynamic<bool>,
@ -65,6 +68,7 @@ impl<'window> RunningWindow<'window> {
) -> Self {
Self {
window,
kludgine_id,
invalidation_status: invalidation_status.clone(),
cushy: cushy.clone(),
focused: focused.clone(),
@ -73,6 +77,14 @@ impl<'window> RunningWindow<'window> {
}
}
/// Returns the [`KludgineId`] of this window.
///
/// Each window has its own unique `KludgineId`.
#[must_use]
pub const fn kludgine_id(&self) -> KludgineId {
self.kludgine_id
}
/// Returns a dynamic that is updated whenever this window's focus status
/// changes.
#[must_use]
@ -820,6 +832,7 @@ where
let mut behavior = T::initialize(
&mut RunningWindow::new(
window,
graphics.id(),
&redraw_status,
&cushy,
&focused,
@ -886,6 +899,7 @@ where
let resizable = window.winit().is_resizable();
let mut window = RunningWindow::new(
window,
graphics.id(),
&self.redraw_status,
&self.cushy,
&self.focused,
@ -1012,13 +1026,14 @@ where
fn close_requested(
&mut self,
window: kludgine::app::Window<'_, WindowCommand>,
_kludgine: &mut Kludgine,
kludgine: &mut Kludgine,
) -> bool {
Self::request_close(
&mut self.should_close,
&mut self.behavior,
&mut RunningWindow::new(
window,
kludgine.id(),
&self.redraw_status,
&self.cushy,
&self.focused,
@ -1093,6 +1108,7 @@ where
};
let mut window = RunningWindow::new(
window,
kludgine.id(),
&self.redraw_status,
&self.cushy,
&self.focused,
@ -1137,6 +1153,7 @@ where
let mut window = RunningWindow::new(
window,
kludgine.id(),
&self.redraw_status,
&self.cushy,
&self.focused,
@ -1173,6 +1190,7 @@ where
.unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget"));
let mut window = RunningWindow::new(
window,
kludgine.id(),
&self.redraw_status,
&self.cushy,
&self.focused,
@ -1206,6 +1224,7 @@ where
let mut window = RunningWindow::new(
window,
kludgine.id(),
&self.redraw_status,
&self.cushy,
&self.focused,
@ -1258,6 +1277,7 @@ where
if self.cursor.widget.take().is_some() {
let mut window = RunningWindow::new(
window,
kludgine.id(),
&self.redraw_status,
&self.cushy,
&self.focused,
@ -1288,6 +1308,7 @@ where
) {
let mut window = RunningWindow::new(
window,
kludgine.id(),
&self.redraw_status,
&self.cushy,
&self.focused,
@ -1390,7 +1411,7 @@ where
fn event(
&mut self,
mut window: kludgine::app::Window<'_, WindowCommand>,
_kludgine: &mut Kludgine,
kludgine: &mut Kludgine,
event: WindowCommand,
) {
match event {
@ -1400,6 +1421,7 @@ where
WindowCommand::RequestClose => {
let mut window = RunningWindow::new(
window,
kludgine.id(),
&self.redraw_status,
&self.cushy,
&self.focused,
@ -1741,3 +1763,46 @@ struct PendingWindowHandle {
handle: OnceLock<kludgine::app::WindowHandle<WindowCommand>>,
commands: Mutex<Vec<WindowCommand>>,
}
/// A collection that stores an instance of `T` per window.
///
/// This is a convenience wrapper around a `HashMap<KludgineId, T>`.
#[derive(Debug, Clone)]
pub struct WindowLocal<T> {
by_window: AHashMap<KludgineId, T>,
}
impl<T> WindowLocal<T> {
/// Looks up the entry for this window.
///
/// Internally this API uses [`HashMap::entry`](hash_map::HashMap::entry).
pub fn entry(&mut self, context: &WidgetContext<'_, '_>) -> hash_map::Entry<'_, KludgineId, T> {
self.by_window.entry(context.kludgine_id())
}
/// Sets `value` as the local value for `context`'s window.
pub fn set(&mut self, context: &WidgetContext<'_, '_>, value: T) {
self.by_window.insert(context.kludgine_id(), value);
}
/// Looks up the value for this window, returning None if not found.
///
/// Internally this API uses [`HashMap::get`](hash_map::HashMap::get).
#[must_use]
pub fn get(&self, context: &WidgetContext<'_, '_>) -> Option<&T> {
self.by_window.get(&context.kludgine_id())
}
/// Removes any stored value for this window.
pub fn clear_for(&mut self, context: &WidgetContext<'_, '_>) -> Option<T> {
self.by_window.remove(&context.kludgine_id())
}
}
impl<T> Default for WindowLocal<T> {
fn default() -> Self {
Self {
by_window: AHashMap::default(),
}
}
}