Grid remove fix, new dynamic features

This commit is contained in:
Jonathan Johnson 2023-12-28 17:36:52 -08:00
parent 285c92f82b
commit c6d66a3166
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
4 changed files with 115 additions and 19 deletions

View file

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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.
### Added
@ -27,6 +28,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
focused widget will be activated and deactived by the events. This previously
was a `Button`-specific behavior that has been refactored into an automatic
behavior for all widgets.
- `GridWidgets` now implements `FromIterator` for types that implement
`Into<GridSection<N>>`.
- `Window::titled` allows setting a window's title, and can be provided a
string-type or a `Dynamic<String>` to allow updating the title while the
window is open.
- `Dynamic::weak_clone` returns a new dynamic that is updated from the original
dynamic, but is careful to not add any strong references to the source
`Dynamic`. This allows breaking dynamic graphs into independent "sections"
that can be deallocated independently of other graphs it is connected with.
- `DynamicReader::on_disconnect` allows attaching a callback that is invoked
once the final source `Dynamic` is dropped.
[99]: https://github.com/khonsulabs/cushy/issues/99

View file

@ -19,7 +19,9 @@ use kempt::{Map, Sort};
use crate::animation::{AnimationHandle, DynamicTransition, IntoAnimate, LinearInterpolate, Spawn};
use crate::context::{self, WidgetContext};
use crate::utils::{run_in_bg, IgnorePoison, WithClone};
use crate::widget::{Children, MakeWidget, MakeWidgetWithTag, WidgetId, WidgetInstance};
use crate::widget::{
Children, MakeWidget, MakeWidgetWithTag, OnceCallback, WidgetId, WidgetInstance,
};
use crate::widgets::{Radio, Select, Space, Switcher};
use crate::window::WindowHandle;
@ -41,6 +43,7 @@ impl<T> Dynamic<T> {
readers: 0,
wakers: Vec::new(),
widgets: AHashSet::new(),
on_disconnect: Vec::new(),
source_callback: CallbackHandle::default(),
}),
during_callback_state: Mutex::default(),
@ -234,6 +237,28 @@ impl<T> Dynamic<T> {
self.map_each(|value| U::from(value))
}
/// Returns a new dynamic that contains a weak clone of `self`'s contents.
///
/// The returned `Dynamic` does not use any strong references, ensuring the
/// returned dynamic does not extend the lifetime of `self`.
#[must_use]
pub fn weak_clone(&self) -> Self
where
T: Clone + PartialEq + Send + 'static,
{
let weak_source = self.downgrade();
let weak_out = Dynamic::new(self.get());
weak_out.set_source(self.0.for_each({
let weak_out = weak_out.clone();
move || {
if let Some(source) = weak_source.upgrade() {
weak_out.set(source.get());
}
}
}));
weak_out
}
/// Attaches `for_each` to this value so that it is invoked each time the
/// value's contents are updated.
pub fn for_each<F>(&self, mut for_each: F) -> CallbackHandle
@ -756,9 +781,15 @@ impl<T> Clone for Dynamic<T> {
impl<T> Drop for Dynamic<T> {
fn drop(&mut self) {
let state = self.state().expect("deadlocked");
if state.readers == 0 {
let mut state = self.state().expect("deadlocked");
if Arc::strong_count(&self.0) == state.readers + 1 {
let on_disconnect = std::mem::take(&mut state.on_disconnect);
drop(state);
for on_disconnect in on_disconnect {
on_disconnect.invoke(());
}
self.0.sync.notify_all();
}
}
@ -1117,6 +1148,7 @@ struct State<T> {
windows: AHashSet<WindowHandle>,
widgets: AHashSet<(WindowHandle, WidgetId)>,
wakers: Vec<Waker>,
on_disconnect: Vec<OnceCallback>,
readers: usize,
}
@ -1550,6 +1582,12 @@ impl<T> DynamicReader<T> {
}
}
/// Returns true if this reader still has any writers connected to it.
#[must_use]
pub fn connected(&self) -> bool {
self.source.state.lock().ignore_poison().readers < Arc::strong_count(&self.source)
}
/// Suspends the current async task until the contained value has been
/// updated or there are no remaining writers for the value.
///
@ -1557,6 +1595,26 @@ impl<T> DynamicReader<T> {
pub fn wait_until_updated(&mut self) -> BlockUntilUpdatedFuture<'_, T> {
BlockUntilUpdatedFuture(self)
}
/// Invokes `on_disconnect` when no instances of `Dynamic<T>` exist.
///
/// This callback will be invoked even if this `DynamicReader` has been
/// dropped.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
pub fn on_disconnect<OnDisconnect>(&self, on_disconnect: OnDisconnect)
where
OnDisconnect: FnOnce() + Send + 'static,
{
self.source
.state()
.expect("deadlocked")
.on_disconnect
.push(OnceCallback::new(|()| on_disconnect()));
}
}
impl<T> context::sealed::Trackable for DynamicReader<T> {

View file

@ -69,10 +69,10 @@ impl<const ELEMENTS: usize> Grid<ELEMENTS> {
fn synchronize_specs(&mut self, context: &mut EventContext<'_, '_>) {
let current_generation = self.columns.generation();
if current_generation.map_or_else(
|| self.layout.children.len() != ELEMENTS,
|gen| Some(gen) != self.spec_generation,
) {
let count_changed = self.layout.children.len() != ELEMENTS;
if count_changed
|| current_generation.map_or_else(|| true, |gen| Some(gen) != self.spec_generation)
{
self.spec_generation = current_generation;
self.columns.map(|columns| {
self.layout.truncate(0);
@ -822,6 +822,15 @@ where
}
}
impl<A, const N: usize> FromIterator<A> for GridWidgets<N>
where
A: Into<GridSection<N>>,
{
fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
Self(iter.into_iter().map(A::into).collect())
}
}
impl<const N: usize> Deref for GridWidgets<N> {
type Target = Vec<GridSection<N>>;

View file

@ -139,7 +139,7 @@ pub type WindowAttributes = kludgine::app::WindowAttributes;
/// A Cushy window that is not yet running.
#[must_use]
pub struct Window<Behavior>
pub struct Window<Behavior = WidgetInstance>
where
Behavior: WindowBehavior,
{
@ -147,6 +147,8 @@ where
pending: PendingWindow,
/// The attributes of this window.
pub attributes: WindowAttributes,
/// The title to display in the title bar of the window.
pub title: Value<String>,
/// The colors to use to theme the user interface.
pub theme: Value<ThemePair>,
/// When true, the system fonts will be loaded into the font database. This
@ -280,6 +282,12 @@ impl Window<WidgetInstance> {
self.on_closed = Some(OnceCallback::new(|()| on_close()));
self
}
/// Sets the window's title.
pub fn titled(mut self, title: impl IntoValue<String>) -> Self {
self.title = title.into_value();
self
}
}
impl<Behavior> Window<Behavior>
@ -310,10 +318,8 @@ where
.clone();
Self {
pending,
attributes: WindowAttributes {
title,
..WindowAttributes::default()
},
title: Value::Constant(title),
attributes: WindowAttributes::default(),
on_closed: None,
context,
load_system_fonts: true,
@ -363,6 +369,7 @@ where
user: self.context,
settings: RefCell::new(sealed::WindowSettings {
cushy,
title: self.title,
redraw_status: self.pending.0.redraw_status.clone(),
on_closed: self.on_closed,
transparent: self.attributes.transparent,
@ -781,6 +788,14 @@ where
context: Self::Context,
) -> Self {
let mut settings = context.settings.borrow_mut();
if let Value::Dynamic(title) = &settings.title {
let handle = window.handle();
title
.for_each_cloned(move |title| {
let _result = handle.send(WindowCommand::SetTitle(title));
})
.persist();
}
let cushy = settings.cushy.clone();
let occluded = settings.occluded.take().unwrap_or_default();
let focused = settings.focused.take().unwrap_or_default();
@ -985,15 +1000,12 @@ where
}
fn initial_window_attributes(context: &Self::Context) -> kludgine::app::WindowAttributes {
let mut attrs = context
.settings
.borrow_mut()
.attributes
.take()
.expect("called more than once");
if let Some(Value::Constant(theme_mode)) = &context.settings.borrow().theme_mode {
let mut settings = context.settings.borrow_mut();
let mut attrs = settings.attributes.take().expect("called more than once");
if let Some(Value::Constant(theme_mode)) = &settings.theme_mode {
attrs.preferred_theme = Some((*theme_mode).into());
}
attrs.title = settings.title.get();
attrs
}
@ -1398,6 +1410,9 @@ where
window.close();
}
}
WindowCommand::SetTitle(new_title) => {
window.set_title(&new_title);
}
}
}
}
@ -1449,6 +1464,7 @@ pub(crate) mod sealed {
pub struct WindowSettings {
pub cushy: Cushy,
pub redraw_status: InvalidationStatus,
pub title: Value<String>,
pub attributes: Option<WindowAttributes>,
pub occluded: Option<Dynamic<bool>>,
pub focused: Option<Dynamic<bool>>,
@ -1469,6 +1485,7 @@ pub(crate) mod sealed {
pub enum WindowCommand {
Redraw,
RequestClose,
SetTitle(String),
}
}