mirror of
https://github.com/danbulant/cushy
synced 2026-06-24 17:12:11 +00:00
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:
parent
999f920f8c
commit
9e4e079bf5
16 changed files with 309 additions and 133 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -7,12 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## Unreleased
|
## 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
|
### Fixed
|
||||||
|
|
||||||
- The root widget is now included in the search for widgets to accept focus.
|
- 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
|
- Widgets that have been laid out with a 0px width or height no longer have
|
||||||
their `redraw` functions called nor can they receive focus.
|
their `redraw` functions called nor can they receive focus.
|
||||||
- `Grid` now synchronizes removal of widgets from `GridWidgets` correctly.
|
- `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
|
### Added
|
||||||
|
|
||||||
|
|
@ -43,6 +64,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
existence.
|
existence.
|
||||||
- `Dynamic::readers()` returns the number of `DynamicReader`s for the dynamic in
|
- `Dynamic::readers()` returns the number of `DynamicReader`s for the dynamic in
|
||||||
existence.
|
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
|
[99]: https://github.com/khonsulabs/cushy/issues/99
|
||||||
|
|
||||||
|
|
|
||||||
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -35,9 +35,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.6"
|
version = "0.8.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
|
checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
|
@ -1155,9 +1155,9 @@ checksum = "d7e253b574775d0ebd7975c471fc18f72f0775a4d42b563b5fbc3c4068aa1075"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kempt"
|
name = "kempt"
|
||||||
version = "0.2.2"
|
version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13a912e97bbe1ec7cbead29896008766dedc4790350c55aece45998fde067200"
|
checksum = "4a37a6bdb52eae6acb1efbab069af766555a6d77712380fd48876b83543d3071"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "khronos-egl"
|
name = "khronos-egl"
|
||||||
|
|
@ -1179,7 +1179,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kludgine"
|
name = "kludgine"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
source = "git+https://github.com/khonsulabs/kludgine#a38466b021ce55c26c6d533191a87a22109016b2"
|
source = "git+https://github.com/khonsulabs/kludgine#0bda2a6dc273aa49338f23ea5190aefdf037d740"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"alot",
|
"alot",
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,32 @@ const INTRO: &str = "This example demonstrates the DebugContext, which allows ob
|
||||||
|
|
||||||
fn main() -> cushy::Result {
|
fn main() -> cushy::Result {
|
||||||
let app = PendingApp::default();
|
let app = PendingApp::default();
|
||||||
let dbg = DebugContext::default();
|
let info = DebugContext::default();
|
||||||
let window_count = Dynamic::new(0_usize);
|
|
||||||
let total_windows = Dynamic::new(0_usize);
|
|
||||||
|
|
||||||
dbg.observe("Open Windows", &window_count);
|
let window_count = Dynamic::new(0_usize);
|
||||||
dbg.observe("Total Windows", &total_windows);
|
let total_windows = info.dbg("Total Windows", Dynamic::new(0_usize));
|
||||||
dbg.clone().open(&app)?;
|
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
|
INTRO
|
||||||
.and("Open a Window".into_button().on_click({
|
.and(open_window_button)
|
||||||
let app = app.as_app();
|
|
||||||
|
|
||||||
move |()| open_a_window(&window_count, &total_windows, &dbg, &app)
|
|
||||||
}))
|
|
||||||
.into_rows()
|
.into_rows()
|
||||||
.centered()
|
.centered()
|
||||||
.run_in(app)
|
.run_in(app)
|
||||||
|
|
@ -30,7 +42,7 @@ fn main() -> cushy::Result {
|
||||||
fn open_a_window(
|
fn open_a_window(
|
||||||
window_count: &Dynamic<usize>,
|
window_count: &Dynamic<usize>,
|
||||||
total_windows: &Dynamic<usize>,
|
total_windows: &Dynamic<usize>,
|
||||||
dbg: &DebugContext,
|
info: &DebugContext,
|
||||||
app: &dyn Application,
|
app: &dyn Application,
|
||||||
) {
|
) {
|
||||||
*window_count.lock() += 1;
|
*window_count.lock() += 1;
|
||||||
|
|
@ -39,10 +51,9 @@ fn open_a_window(
|
||||||
*total
|
*total
|
||||||
});
|
});
|
||||||
let window_title = format!("Window #{window_number}");
|
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);
|
let value = dbg.dbg("Slider", Dynamic::new(0_u8));
|
||||||
dbg.observe("Slider", &value);
|
|
||||||
|
|
||||||
let window_count = window_count.clone();
|
let window_count = window_count.clone();
|
||||||
let _ = format!("This is window {window_number}.")
|
let _ = format!("This is window {window_number}.")
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use kludgine::app::winit::event::{
|
||||||
};
|
};
|
||||||
use kludgine::app::winit::window::CursorIcon;
|
use kludgine::app::winit::window::CursorIcon;
|
||||||
use kludgine::shapes::{Shape, StrokeOptions};
|
use kludgine::shapes::{Shape, StrokeOptions};
|
||||||
use kludgine::{Color, Kludgine};
|
use kludgine::{Color, Kludgine, KludgineId};
|
||||||
|
|
||||||
use crate::animation::ZeroToOne;
|
use crate::animation::ZeroToOne;
|
||||||
use crate::graphics::Graphics;
|
use crate::graphics::Graphics;
|
||||||
|
|
@ -24,9 +24,7 @@ use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair};
|
||||||
use crate::tree::Tree;
|
use crate::tree::Tree;
|
||||||
use crate::utils::IgnorePoison;
|
use crate::utils::IgnorePoison;
|
||||||
use crate::value::{IntoValue, Value};
|
use crate::value::{IntoValue, Value};
|
||||||
use crate::widget::{
|
use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance};
|
||||||
EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance, WidgetRef,
|
|
||||||
};
|
|
||||||
use crate::window::{CursorState, RunningWindow, ThemeMode};
|
use crate::window::{CursorState, RunningWindow, ThemeMode};
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
|
|
@ -893,6 +891,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
||||||
tree,
|
tree,
|
||||||
effective_styles: current_node.effective_styles(),
|
effective_styles: current_node.effective_styles(),
|
||||||
cache: WidgetCacheKey {
|
cache: WidgetCacheKey {
|
||||||
|
kludgine_id: Some(window.kludgine_id()),
|
||||||
theme_mode,
|
theme_mode,
|
||||||
enabled,
|
enabled,
|
||||||
},
|
},
|
||||||
|
|
@ -941,6 +940,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
||||||
WidgetContext {
|
WidgetContext {
|
||||||
effective_styles,
|
effective_styles,
|
||||||
cache: WidgetCacheKey {
|
cache: WidgetCacheKey {
|
||||||
|
kludgine_id: self.cache.kludgine_id,
|
||||||
theme_mode,
|
theme_mode,
|
||||||
enabled: current_node.enabled(&self.handle()),
|
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 {
|
impl ManageWidget for MountedWidget {
|
||||||
type Managed = Self;
|
type Managed = Self;
|
||||||
|
|
||||||
|
|
@ -1383,6 +1372,7 @@ impl<T> MapManagedWidget<T> for MountedWidget {
|
||||||
/// keys are not equal, the widget should clear all caches.
|
/// keys are not equal, the widget should clear all caches.
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
pub struct WidgetCacheKey {
|
pub struct WidgetCacheKey {
|
||||||
|
kludgine_id: Option<KludgineId>,
|
||||||
theme_mode: ThemeMode,
|
theme_mode: ThemeMode,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -1390,6 +1380,7 @@ pub struct WidgetCacheKey {
|
||||||
impl Default for WidgetCacheKey {
|
impl Default for WidgetCacheKey {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
kludgine_id: None,
|
||||||
theme_mode: ThemeMode::default().inverse(),
|
theme_mode: ThemeMode::default().inverse(),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
src/debug.rs
38
src/debug.rs
|
|
@ -17,23 +17,43 @@ pub struct DebugContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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
|
/// When the final reference to `value` is dropped, this observation will
|
||||||
/// automatically be removed.
|
/// 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
|
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 reader = value.create_reader();
|
||||||
let id = self.section.map_ref(|section| {
|
let id = self.section.map_ref(|section| {
|
||||||
section.values.lock().push(Box::new(RegisteredValue {
|
section.values.lock().push(Box::new(RegisteredValue {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
value: reader.clone(),
|
value: reader.clone(),
|
||||||
widget: value
|
widget: make_observer(value.weak_clone()).make_widget(),
|
||||||
.weak_clone()
|
|
||||||
.map_each(|value| format!("{value:?}"))
|
|
||||||
.make_widget(),
|
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
|
|
@ -79,7 +99,7 @@ impl DebugContext {
|
||||||
fn into_window(self) -> Window {
|
fn into_window(self) -> Window {
|
||||||
self.section
|
self.section
|
||||||
.map_ref(|section| section.widget.clone())
|
.map_ref(|section| section.widget.clone())
|
||||||
.vertical_scroll()
|
// .vertical_scroll()
|
||||||
.into_window()
|
.into_window()
|
||||||
.titled("Cushy Debugger")
|
.titled("Cushy Debugger")
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +193,7 @@ impl DebugSection {
|
||||||
let value_grid = Grid::from_rows(values.map_each(|values| {
|
let value_grid = Grid::from_rows(values.map_each(|values| {
|
||||||
values
|
values
|
||||||
.iter()
|
.iter()
|
||||||
.map(|o| [o.label().make_widget(), o.widget().clone()])
|
.map(|o| (o.label(), o.widget().clone().align_left()))
|
||||||
.collect::<GridWidgets<2>>()
|
.collect::<GridWidgets<2>>()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
34
src/tree.rs
34
src/tree.rs
|
|
@ -83,15 +83,17 @@ impl Tree {
|
||||||
|
|
||||||
let node = &mut data.nodes[widget];
|
let node = &mut data.nodes[widget];
|
||||||
node.layout = Some(rect);
|
node.layout = Some(rect);
|
||||||
let mut children_to_offset = node.children.clone();
|
if !node.children.is_empty() {
|
||||||
while let Some(child) = children_to_offset.pop() {
|
let mut children_to_offset = node.children.clone();
|
||||||
if let Some(layout) = data
|
while let Some(child) = children_to_offset.pop() {
|
||||||
.nodes
|
if let Some(layout) = data
|
||||||
.get_mut(child)
|
.nodes
|
||||||
.and_then(|child| child.layout.as_mut())
|
.get_mut(child)
|
||||||
{
|
.and_then(|child| child.layout.as_mut())
|
||||||
layout.origin += rect.origin;
|
{
|
||||||
children_to_offset.extend(data.nodes[child].children.iter().copied());
|
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(crate) struct HoverResults {
|
||||||
pub unhovered: Vec<MountedWidget>,
|
pub unhovered: Vec<MountedWidget>,
|
||||||
pub hovered: Vec<MountedWidget>,
|
pub hovered: Vec<MountedWidget>,
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ impl<T> Dynamic<T> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn weak_clone(&self) -> Self
|
pub fn weak_clone(&self) -> Self
|
||||||
where
|
where
|
||||||
T: Clone + PartialEq + Send + 'static,
|
T: Clone + Send + 'static,
|
||||||
{
|
{
|
||||||
let weak_source = self.downgrade();
|
let weak_source = self.downgrade();
|
||||||
let weak_out = Dynamic::new(self.get());
|
let weak_out = Dynamic::new(self.get());
|
||||||
|
|
@ -276,7 +276,7 @@ impl<T> Dynamic<T> {
|
||||||
let weak_out = weak_out.clone();
|
let weak_out = weak_out.clone();
|
||||||
move || {
|
move || {
|
||||||
if let Some(source) = weak_source.upgrade() {
|
if let Some(source) = weak_source.upgrade() {
|
||||||
weak_out.set(source.get());
|
*weak_out.lock() = source.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ use kludgine::app::winit::window::CursorIcon;
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
|
|
||||||
use crate::app::{Application, Open, PendingApp, Run};
|
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::{
|
use crate::styles::components::{
|
||||||
FontFamily, FontStyle, FontWeight, Heading1FontFamily, Heading1Style, Heading1Weight,
|
FontFamily, FontStyle, FontWeight, Heading1FontFamily, Heading1Style, Heading1Weight,
|
||||||
Heading2FontFamily, Heading2Style, Heading2Weight, Heading3FontFamily, Heading3Style,
|
Heading2FontFamily, Heading2Style, Heading2Weight, Heading3FontFamily, Heading3Style,
|
||||||
|
|
@ -42,7 +44,7 @@ use crate::widgets::{
|
||||||
Align, Button, Checkbox, Collapse, Container, Disclose, Expand, Layers, Resize, Scroll, Space,
|
Align, Button, Checkbox, Collapse, Container, Disclose, Expand, Layers, Resize, Scroll, Space,
|
||||||
Stack, Style, Themed, ThemedMode, Validated, Wrap,
|
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;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
/// A type that makes up a graphical user interface.
|
/// 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.
|
/// The widget has been removed from its parent widget.
|
||||||
#[allow(unused_variables)]
|
#[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`.
|
/// Returns true if this widget should respond to mouse input at `location`.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
|
|
@ -2237,24 +2241,34 @@ impl MountableChild for MountedWidget {
|
||||||
|
|
||||||
/// A child widget
|
/// A child widget
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum WidgetRef {
|
pub struct WidgetRef {
|
||||||
/// An unmounted child widget
|
instance: WidgetInstance,
|
||||||
Unmounted(WidgetInstance),
|
mounted: WindowLocal<MountedWidget>,
|
||||||
/// A mounted child widget
|
|
||||||
Mounted(MountedWidget),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetRef {
|
impl WidgetRef {
|
||||||
/// Returns a new unmounted child
|
/// Returns a new unmounted child
|
||||||
pub fn new(widget: impl MakeWidget) -> Self {
|
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.
|
/// Returns this child, mounting it in the process if necessary.
|
||||||
pub fn mount_if_needed<'window>(&mut self, context: &mut impl AsEventContext<'window>) {
|
pub fn mount_if_needed<'window>(&mut self, context: &mut impl AsEventContext<'window>) {
|
||||||
if let WidgetRef::Unmounted(instance) = self {
|
self.mounted_for_context(context);
|
||||||
*self = WidgetRef::Mounted(context.push_child(instance.clone()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns this child, mounting it in the process if necessary.
|
/// Returns this child, mounting it in the process if necessary.
|
||||||
|
|
@ -2262,39 +2276,39 @@ impl WidgetRef {
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &mut impl AsEventContext<'window>,
|
context: &mut impl AsEventContext<'window>,
|
||||||
) -> MountedWidget {
|
) -> MountedWidget {
|
||||||
self.mount_if_needed(context);
|
self.mounted_for_context(context).clone()
|
||||||
|
}
|
||||||
|
|
||||||
let Self::Mounted(widget) = self else {
|
/// Returns this child, mounting it in the process if necessary.
|
||||||
unreachable!("just initialized")
|
#[must_use]
|
||||||
};
|
pub fn as_mounted(&self, context: &WidgetContext<'_, '_>) -> Option<&MountedWidget> {
|
||||||
widget.clone()
|
self.mounted.get(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the a reference to the underlying widget instance.
|
/// Returns the a reference to the underlying widget instance.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn widget(&self) -> &WidgetInstance {
|
pub const fn widget(&self) -> &WidgetInstance {
|
||||||
match self {
|
&self.instance
|
||||||
WidgetRef::Unmounted(widget) => widget,
|
}
|
||||||
WidgetRef::Mounted(managed) => &managed.widget,
|
|
||||||
|
/// 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 {
|
impl AsRef<WidgetId> for WidgetRef {
|
||||||
fn as_ref(&self) -> &WidgetId {
|
fn as_ref(&self) -> &WidgetId {
|
||||||
match self {
|
self.instance.as_ref()
|
||||||
WidgetRef::Unmounted(widget) => widget.as_ref(),
|
|
||||||
WidgetRef::Mounted(widget) => widget.as_ref(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for WidgetRef {
|
impl Debug for WidgetRef {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
Debug::fmt(&self.instance, f)
|
||||||
Self::Unmounted(arg0) => Debug::fmt(arg0, f),
|
|
||||||
Self::Mounted(arg0) => Debug::fmt(arg0, f),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2302,11 +2316,18 @@ impl Eq for WidgetRef {}
|
||||||
|
|
||||||
impl PartialEq for WidgetRef {
|
impl PartialEq for WidgetRef {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
if let (WidgetRef::Mounted(this), WidgetRef::Mounted(other)) = (self, other) {
|
self.instance == other.instance
|
||||||
this == other
|
}
|
||||||
} else {
|
}
|
||||||
self.widget() == other.widget()
|
|
||||||
}
|
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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ use crate::styles::components::{
|
||||||
use crate::styles::{ColorExt, Styles};
|
use crate::styles::{ColorExt, Styles};
|
||||||
use crate::value::{Dynamic, IntoValue, Value};
|
use crate::value::{Dynamic, IntoValue, Value};
|
||||||
use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED};
|
use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED};
|
||||||
|
use crate::window::WindowLocal;
|
||||||
use crate::FitMeasuredSize;
|
use crate::FitMeasuredSize;
|
||||||
|
|
||||||
/// A clickable button.
|
/// A clickable button.
|
||||||
|
|
@ -33,13 +34,18 @@ pub struct Button {
|
||||||
/// The kind of button to draw.
|
/// The kind of button to draw.
|
||||||
pub kind: Value<ButtonKind>,
|
pub kind: Value<ButtonKind>,
|
||||||
focusable: bool,
|
focusable: bool,
|
||||||
|
per_window: WindowLocal<PerWindow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct PerWindow {
|
||||||
buttons_pressed: usize,
|
buttons_pressed: usize,
|
||||||
cached_state: CacheState,
|
cached_state: CacheState,
|
||||||
active_colors: Option<Dynamic<ButtonColors>>,
|
active_colors: Option<Dynamic<ButtonColors>>,
|
||||||
color_animation: AnimationHandle,
|
color_animation: AnimationHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
#[derive(Default, Debug, Eq, PartialEq, Clone, Copy)]
|
||||||
struct CacheState {
|
struct CacheState {
|
||||||
key: WidgetCacheKey,
|
key: WidgetCacheKey,
|
||||||
kind: ButtonKind,
|
kind: ButtonKind,
|
||||||
|
|
@ -133,14 +139,8 @@ impl Button {
|
||||||
Self {
|
Self {
|
||||||
content: content.widget_ref(),
|
content: content.widget_ref(),
|
||||||
on_click: None,
|
on_click: None,
|
||||||
cached_state: CacheState {
|
per_window: WindowLocal::default(),
|
||||||
key: WidgetCacheKey::default(),
|
|
||||||
kind: ButtonKind::default(),
|
|
||||||
},
|
|
||||||
buttons_pressed: 0,
|
|
||||||
active_colors: None,
|
|
||||||
kind: Value::Constant(ButtonKind::default()),
|
kind: Value::Constant(ButtonKind::default()),
|
||||||
color_animation: AnimationHandle::default(),
|
|
||||||
focusable: true,
|
focusable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -229,7 +229,9 @@ impl Button {
|
||||||
let kind = self.kind.get_tracking_redraw(context);
|
let kind = self.kind.get_tracking_redraw(context);
|
||||||
let visual_state = Self::visual_style(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(),
|
key: context.cache_key(),
|
||||||
kind,
|
kind,
|
||||||
};
|
};
|
||||||
|
|
@ -247,33 +249,46 @@ impl Button {
|
||||||
|
|
||||||
fn update_colors(&mut self, context: &mut WidgetContext<'_, '_>, immediate: bool) {
|
fn update_colors(&mut self, context: &mut WidgetContext<'_, '_>, immediate: bool) {
|
||||||
let new_style = self.determine_stateful_colors(context);
|
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)) => {
|
(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))
|
.over(Duration::from_millis(150))
|
||||||
.with_easing(context.get(&Easing))
|
.with_easing(context.get(&Easing))
|
||||||
.spawn();
|
.spawn();
|
||||||
}
|
}
|
||||||
(true, Some(style)) => {
|
(true, Some(style)) => {
|
||||||
style.set(new_style);
|
style.set(new_style);
|
||||||
self.color_animation.clear();
|
window_local.color_animation.clear();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let new_style = Dynamic::new(new_style);
|
let new_style = Dynamic::new(new_style);
|
||||||
let foreground = new_style.map_each(|s| s.foreground);
|
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));
|
context.attach_styles(Styles::new().with(&TextColor, foreground));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_style(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors {
|
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);
|
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);
|
context.redraw_when_changed(style);
|
||||||
style.get()
|
style.get()
|
||||||
}
|
}
|
||||||
|
|
@ -353,7 +368,10 @@ impl Widget for Button {
|
||||||
#![allow(clippy::similar_names)]
|
#![allow(clippy::similar_names)]
|
||||||
|
|
||||||
let current_style = self.kind.get_tracking_redraw(context);
|
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);
|
self.update_colors(context, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,7 +432,7 @@ impl Widget for Button {
|
||||||
_button: MouseButton,
|
_button: MouseButton,
|
||||||
context: &mut EventContext<'_, '_>,
|
context: &mut EventContext<'_, '_>,
|
||||||
) -> EventHandling {
|
) -> EventHandling {
|
||||||
self.buttons_pressed += 1;
|
self.per_window.entry(context).or_default().buttons_pressed += 1;
|
||||||
context.activate();
|
context.activate();
|
||||||
HANDLED
|
HANDLED
|
||||||
}
|
}
|
||||||
|
|
@ -446,8 +464,9 @@ impl Widget for Button {
|
||||||
_button: MouseButton,
|
_button: MouseButton,
|
||||||
context: &mut EventContext<'_, '_>,
|
context: &mut EventContext<'_, '_>,
|
||||||
) {
|
) {
|
||||||
self.buttons_pressed -= 1;
|
let window_local = self.per_window.entry(context).or_default();
|
||||||
if self.buttons_pressed == 0 {
|
window_local.buttons_pressed -= 1;
|
||||||
|
if window_local.buttons_pressed == 0 {
|
||||||
context.deactivate();
|
context.deactivate();
|
||||||
|
|
||||||
if let (true, Some(location)) = (self.focusable, location) {
|
if let (true, Some(location)) = (self.focusable, location) {
|
||||||
|
|
@ -472,7 +491,7 @@ impl Widget for Button {
|
||||||
.into_upx(context.gfx.scale())
|
.into_upx(context.gfx.scale())
|
||||||
.round();
|
.round();
|
||||||
let double_padding = padding * 2;
|
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 available_space = available_space.map(|space| space - double_padding);
|
||||||
let size = context.for_other(&mounted).layout(available_space);
|
let size = context.for_other(&mounted).layout(available_space);
|
||||||
let size = available_space.fit_measured(size, context.gfx.scale());
|
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<'_, '_>) {
|
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
|
// If we have no buttons pressed, the event should fire on activate not
|
||||||
// on deactivate.
|
// on deactivate.
|
||||||
if self.buttons_pressed == 0 {
|
if window_local.buttons_pressed == 0 {
|
||||||
self.invoke_on_click(context);
|
self.invoke_on_click(context);
|
||||||
}
|
}
|
||||||
self.update_colors(context, true);
|
self.update_colors(context, true);
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ impl DiscloseIndicator {
|
||||||
contents: WidgetRef::new(contents.collapse_vertically(collapsed.clone())),
|
contents: WidgetRef::new(contents.collapse_vertically(collapsed.clone())),
|
||||||
collapsed,
|
collapsed,
|
||||||
hovering_indicator: false,
|
hovering_indicator: false,
|
||||||
label: label.map(WidgetRef::Unmounted),
|
label: label.map(WidgetRef::new),
|
||||||
target_colors: None,
|
target_colors: None,
|
||||||
color: Dynamic::new(Color::CLEAR_WHITE),
|
color: Dynamic::new(Color::CLEAR_WHITE),
|
||||||
stroke_color: Dynamic::new(Color::CLEAR_WHITE),
|
stroke_color: Dynamic::new(Color::CLEAR_WHITE),
|
||||||
|
|
|
||||||
|
|
@ -381,7 +381,7 @@ impl GridLayout {
|
||||||
scale: Fraction,
|
scale: Fraction,
|
||||||
mut measure: impl FnMut(usize, usize, Size<ConstraintLimit>, bool) -> Size<UPx>,
|
mut measure: impl FnMut(usize, usize, Size<ConstraintLimit>, bool) -> Size<UPx>,
|
||||||
) -> 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 available_space = space_constraint.max();
|
||||||
let known_gutters = gutter.saturating_mul(UPx::new(
|
let known_gutters = gutter.saturating_mul(UPx::new(
|
||||||
(self.children.len() - self.fit_to_content.len())
|
(self.children.len() - self.fit_to_content.len())
|
||||||
|
|
@ -391,6 +391,13 @@ impl GridLayout {
|
||||||
let allocated_space =
|
let allocated_space =
|
||||||
self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil() + known_gutters;
|
self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil() + known_gutters;
|
||||||
let mut remaining = available_space.saturating_sub(allocated_space);
|
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
|
// 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
|
// widgets an opportunity to lay themselves out in the full area. This
|
||||||
// requires one extra layout call, so we avoid persisting layouts during
|
// requires one extra layout call, so we avoid persisting layouts during
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use crate::context::{GraphicsContext, LayoutContext};
|
||||||
use crate::styles::components::TextColor;
|
use crate::styles::components::TextColor;
|
||||||
use crate::value::{Dynamic, Generation, IntoValue, Value};
|
use crate::value::{Dynamic, Generation, IntoValue, Value};
|
||||||
use crate::widget::{Widget, WidgetInstance};
|
use crate::widget::{Widget, WidgetInstance};
|
||||||
|
use crate::window::WindowLocal;
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
/// A read-only text widget.
|
/// A read-only text widget.
|
||||||
|
|
@ -16,7 +17,7 @@ use crate::ConstraintLimit;
|
||||||
pub struct Label {
|
pub struct Label {
|
||||||
/// The contents of the label.
|
/// The contents of the label.
|
||||||
pub text: Value<String>,
|
pub text: Value<String>,
|
||||||
prepared_text: Option<(MeasuredText<Px>, Option<Generation>, Px, Color)>,
|
prepared_text: WindowLocal<(MeasuredText<Px>, Option<Generation>, Px, Color)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Label {
|
impl Label {
|
||||||
|
|
@ -24,7 +25,7 @@ impl Label {
|
||||||
pub fn new(text: impl IntoValue<String>) -> Self {
|
pub fn new(text: impl IntoValue<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: text.into_value(),
|
text: text.into_value(),
|
||||||
prepared_text: None,
|
prepared_text: WindowLocal::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,14 +36,15 @@ impl Label {
|
||||||
width: Px,
|
width: Px,
|
||||||
) -> &MeasuredText<Px> {
|
) -> &MeasuredText<Px> {
|
||||||
let check_generation = self.text.generation();
|
let check_generation = self.text.generation();
|
||||||
match &self.prepared_text {
|
match self.prepared_text.get(context) {
|
||||||
Some((prepared, prepared_generation, prepared_width, prepared_color))
|
Some((prepared, prepared_generation, prepared_width, prepared_color))
|
||||||
if prepared.can_render_to(&context.gfx)
|
if prepared.can_render_to(&context.gfx)
|
||||||
&& *prepared_generation == check_generation
|
&& *prepared_generation == check_generation
|
||||||
&& *prepared_color == color
|
&& *prepared_color == color
|
||||||
&& (*prepared_width == width
|
&& (*prepared_width == width
|
||||||
|| ((*prepared_width < width || prepared.size.width <= width)
|
|| (*prepared_width < width
|
||||||
&& prepared.line_height == prepared.size.height)) => {}
|
|| (prepared.size.width <= width
|
||||||
|
&& prepared.line_height == prepared.size.height))) => {}
|
||||||
_ => {
|
_ => {
|
||||||
context.apply_current_font_settings();
|
context.apply_current_font_settings();
|
||||||
let measured = self.text.map(|text| {
|
let measured = self.text.map(|text| {
|
||||||
|
|
@ -50,12 +52,13 @@ impl Label {
|
||||||
.gfx
|
.gfx
|
||||||
.measure_text(Text::new(text, color).wrap_at(width))
|
.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
|
self.prepared_text
|
||||||
.as_ref()
|
.get(context)
|
||||||
.map(|(prepared, _, _, _)| prepared)
|
.map(|(prepared, _, _, _)| prepared)
|
||||||
.expect("always initialized")
|
.expect("always initialized")
|
||||||
}
|
}
|
||||||
|
|
@ -86,12 +89,16 @@ impl Widget for Label {
|
||||||
let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
|
let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
|
||||||
let prepared = self.prepared_text(context, color, width);
|
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 {
|
fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
fmt.debug_tuple("Label").field(&self.text).finish()
|
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 {
|
macro_rules! impl_make_widget {
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ impl Widget for OverlayLayer {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
|
|
||||||
for child in &state.overlays {
|
for child in &state.overlays {
|
||||||
let WidgetRef::Mounted(mounted) = &child.widget else {
|
let Some(mounted) = child.widget.as_mounted(context) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,10 +92,7 @@ impl Stack {
|
||||||
{
|
{
|
||||||
(child, size)
|
(child, size)
|
||||||
} else {
|
} else {
|
||||||
(
|
(WidgetRef::new(widget.clone()), GridDimension::FitContent)
|
||||||
WidgetRef::Unmounted(widget.clone()),
|
|
||||||
GridDimension::FitContent,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
drop(guard);
|
drop(guard);
|
||||||
this.insert(index, widget.mounted(context));
|
this.insert(index, widget.mounted(context));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::fmt::Debug;
|
||||||
|
|
||||||
use figures::Size;
|
use figures::Size;
|
||||||
|
|
||||||
use crate::context::{AsEventContext, LayoutContext};
|
use crate::context::LayoutContext;
|
||||||
use crate::value::{Dynamic, DynamicReader, IntoDynamic};
|
use crate::value::{Dynamic, DynamicReader, IntoDynamic};
|
||||||
use crate::widget::{WidgetInstance, WidgetRef, WrapperWidget};
|
use crate::widget::{WidgetInstance, WidgetRef, WrapperWidget};
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
@ -55,10 +55,7 @@ impl WrapperWidget for Switcher {
|
||||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||||
) -> Size<ConstraintLimit> {
|
) -> Size<ConstraintLimit> {
|
||||||
if self.source.has_updated() {
|
if self.source.has_updated() {
|
||||||
let removed = std::mem::replace(&mut self.child, WidgetRef::new(self.source.get()));
|
self.child.unmount_in(context);
|
||||||
if let WidgetRef::Mounted(removed) = removed {
|
|
||||||
context.remove_child(&removed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
context.invalidate_when_changed(&self.source);
|
context.invalidate_when_changed(&self.source);
|
||||||
available_space
|
available_space
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! Types for displaying a [`Widget`] inside of a desktop window.
|
//! Types for displaying a [`Widget`] inside of a desktop window.
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
use std::collections::hash_map;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::ops::{Deref, DerefMut, Not};
|
use std::ops::{Deref, DerefMut, Not};
|
||||||
|
|
@ -23,7 +24,7 @@ use kludgine::app::WindowBehavior as _;
|
||||||
use kludgine::cosmic_text::{fontdb, Family, FamilyOwned};
|
use kludgine::cosmic_text::{fontdb, Family, FamilyOwned};
|
||||||
use kludgine::render::Drawing;
|
use kludgine::render::Drawing;
|
||||||
use kludgine::wgpu::CompositeAlphaMode;
|
use kludgine::wgpu::CompositeAlphaMode;
|
||||||
use kludgine::Kludgine;
|
use kludgine::{Kludgine, KludgineId};
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
|
||||||
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
|
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
|
||||||
|
|
@ -47,6 +48,7 @@ use crate::{initialize_tracing, ConstraintLimit};
|
||||||
/// A currently running Cushy window.
|
/// A currently running Cushy window.
|
||||||
pub struct RunningWindow<'window> {
|
pub struct RunningWindow<'window> {
|
||||||
window: kludgine::app::Window<'window, WindowCommand>,
|
window: kludgine::app::Window<'window, WindowCommand>,
|
||||||
|
kludgine_id: KludgineId,
|
||||||
invalidation_status: InvalidationStatus,
|
invalidation_status: InvalidationStatus,
|
||||||
cushy: Cushy,
|
cushy: Cushy,
|
||||||
focused: Dynamic<bool>,
|
focused: Dynamic<bool>,
|
||||||
|
|
@ -57,6 +59,7 @@ pub struct RunningWindow<'window> {
|
||||||
impl<'window> RunningWindow<'window> {
|
impl<'window> RunningWindow<'window> {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
window: kludgine::app::Window<'window, WindowCommand>,
|
window: kludgine::app::Window<'window, WindowCommand>,
|
||||||
|
kludgine_id: KludgineId,
|
||||||
invalidation_status: &InvalidationStatus,
|
invalidation_status: &InvalidationStatus,
|
||||||
cushy: &Cushy,
|
cushy: &Cushy,
|
||||||
focused: &Dynamic<bool>,
|
focused: &Dynamic<bool>,
|
||||||
|
|
@ -65,6 +68,7 @@ impl<'window> RunningWindow<'window> {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
window,
|
window,
|
||||||
|
kludgine_id,
|
||||||
invalidation_status: invalidation_status.clone(),
|
invalidation_status: invalidation_status.clone(),
|
||||||
cushy: cushy.clone(),
|
cushy: cushy.clone(),
|
||||||
focused: focused.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
|
/// Returns a dynamic that is updated whenever this window's focus status
|
||||||
/// changes.
|
/// changes.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -820,6 +832,7 @@ where
|
||||||
let mut behavior = T::initialize(
|
let mut behavior = T::initialize(
|
||||||
&mut RunningWindow::new(
|
&mut RunningWindow::new(
|
||||||
window,
|
window,
|
||||||
|
graphics.id(),
|
||||||
&redraw_status,
|
&redraw_status,
|
||||||
&cushy,
|
&cushy,
|
||||||
&focused,
|
&focused,
|
||||||
|
|
@ -886,6 +899,7 @@ where
|
||||||
let resizable = window.winit().is_resizable();
|
let resizable = window.winit().is_resizable();
|
||||||
let mut window = RunningWindow::new(
|
let mut window = RunningWindow::new(
|
||||||
window,
|
window,
|
||||||
|
graphics.id(),
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.cushy,
|
&self.cushy,
|
||||||
&self.focused,
|
&self.focused,
|
||||||
|
|
@ -1012,13 +1026,14 @@ where
|
||||||
fn close_requested(
|
fn close_requested(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: kludgine::app::Window<'_, WindowCommand>,
|
window: kludgine::app::Window<'_, WindowCommand>,
|
||||||
_kludgine: &mut Kludgine,
|
kludgine: &mut Kludgine,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
Self::request_close(
|
Self::request_close(
|
||||||
&mut self.should_close,
|
&mut self.should_close,
|
||||||
&mut self.behavior,
|
&mut self.behavior,
|
||||||
&mut RunningWindow::new(
|
&mut RunningWindow::new(
|
||||||
window,
|
window,
|
||||||
|
kludgine.id(),
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.cushy,
|
&self.cushy,
|
||||||
&self.focused,
|
&self.focused,
|
||||||
|
|
@ -1093,6 +1108,7 @@ where
|
||||||
};
|
};
|
||||||
let mut window = RunningWindow::new(
|
let mut window = RunningWindow::new(
|
||||||
window,
|
window,
|
||||||
|
kludgine.id(),
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.cushy,
|
&self.cushy,
|
||||||
&self.focused,
|
&self.focused,
|
||||||
|
|
@ -1137,6 +1153,7 @@ where
|
||||||
|
|
||||||
let mut window = RunningWindow::new(
|
let mut window = RunningWindow::new(
|
||||||
window,
|
window,
|
||||||
|
kludgine.id(),
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.cushy,
|
&self.cushy,
|
||||||
&self.focused,
|
&self.focused,
|
||||||
|
|
@ -1173,6 +1190,7 @@ where
|
||||||
.unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget"));
|
.unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget"));
|
||||||
let mut window = RunningWindow::new(
|
let mut window = RunningWindow::new(
|
||||||
window,
|
window,
|
||||||
|
kludgine.id(),
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.cushy,
|
&self.cushy,
|
||||||
&self.focused,
|
&self.focused,
|
||||||
|
|
@ -1206,6 +1224,7 @@ where
|
||||||
|
|
||||||
let mut window = RunningWindow::new(
|
let mut window = RunningWindow::new(
|
||||||
window,
|
window,
|
||||||
|
kludgine.id(),
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.cushy,
|
&self.cushy,
|
||||||
&self.focused,
|
&self.focused,
|
||||||
|
|
@ -1258,6 +1277,7 @@ where
|
||||||
if self.cursor.widget.take().is_some() {
|
if self.cursor.widget.take().is_some() {
|
||||||
let mut window = RunningWindow::new(
|
let mut window = RunningWindow::new(
|
||||||
window,
|
window,
|
||||||
|
kludgine.id(),
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.cushy,
|
&self.cushy,
|
||||||
&self.focused,
|
&self.focused,
|
||||||
|
|
@ -1288,6 +1308,7 @@ where
|
||||||
) {
|
) {
|
||||||
let mut window = RunningWindow::new(
|
let mut window = RunningWindow::new(
|
||||||
window,
|
window,
|
||||||
|
kludgine.id(),
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.cushy,
|
&self.cushy,
|
||||||
&self.focused,
|
&self.focused,
|
||||||
|
|
@ -1390,7 +1411,7 @@ where
|
||||||
fn event(
|
fn event(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut window: kludgine::app::Window<'_, WindowCommand>,
|
mut window: kludgine::app::Window<'_, WindowCommand>,
|
||||||
_kludgine: &mut Kludgine,
|
kludgine: &mut Kludgine,
|
||||||
event: WindowCommand,
|
event: WindowCommand,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
|
|
@ -1400,6 +1421,7 @@ where
|
||||||
WindowCommand::RequestClose => {
|
WindowCommand::RequestClose => {
|
||||||
let mut window = RunningWindow::new(
|
let mut window = RunningWindow::new(
|
||||||
window,
|
window,
|
||||||
|
kludgine.id(),
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.cushy,
|
&self.cushy,
|
||||||
&self.focused,
|
&self.focused,
|
||||||
|
|
@ -1741,3 +1763,46 @@ struct PendingWindowHandle {
|
||||||
handle: OnceLock<kludgine::app::WindowHandle<WindowCommand>>,
|
handle: OnceLock<kludgine::app::WindowHandle<WindowCommand>>,
|
||||||
commands: Mutex<Vec<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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue