mirror of
https://github.com/danbulant/cushy
synced 2026-06-14 20:11:04 +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
|
||||
|
||||
### 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
10
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}.")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
38
src/debug.rs
38
src/debug.rs
|
|
@ -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>>()
|
||||
}));
|
||||
|
||||
|
|
|
|||
34
src/tree.rs
34
src/tree.rs
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue