Nested modals

This commit is contained in:
Jonathan Johnson 2024-10-04 10:17:40 -07:00
parent e78d1d28bf
commit 4f3ef7d9ed
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
11 changed files with 326 additions and 85 deletions

View file

@ -67,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
winit, and naga. Thanks to @bluenote10 for the feedback! winit, and naga. Thanks to @bluenote10 for the feedback!
- `WrapperWidget::activate`'s default implementation now activates the wrapped - `WrapperWidget::activate`'s default implementation now activates the wrapped
widget. widget.
- `Space` now intercepts mouse events if its color has a non-zero alpha channel.
### Fixed ### Fixed
@ -91,6 +92,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`Dynamic<T>` change callbacks has been fixed. `Dynamic<T>` change callbacks has been fixed.
- `Stack` no longer unwraps a `Resize` child if the resize widget is resizing in - `Stack` no longer unwraps a `Resize` child if the resize widget is resizing in
the direction opposite of the Stack's orientation. the direction opposite of the Stack's orientation.
- If the layout of widgets changes during a redraw, the currently hovered widget
is now properly updated immediately. Previously, the hover would only update
on the next cursor event.
### Added ### Added
@ -211,6 +215,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Choosing one or more files - Choosing one or more files
- Choosing a single folder/directory - Choosing a single folder/directory
- Choosing one or more folders/directories - Choosing one or more folders/directories
- `DynamicGuard::unlocked` executes a closure while the guard is temporarily
unlocked.
[139]: https://github.com/khonsulabs/cushy/issues/139 [139]: https://github.com/khonsulabs/cushy/issues/139

16
Cargo.lock generated
View file

@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.24.1" version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [ dependencies = [
"gimli", "gimli",
] ]
@ -75,9 +75,9 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]] [[package]]
name = "alot" name = "alot"
version = "0.3.1" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b072fc284b73a3e4154e2decdbaad711daca0e8fedfceb0d7b1cbe2dffb00e2b" checksum = "4c7a3dc3ad32931b2d6e97c99a702208dfd1e2c446580e5f99d1d8355df26db6"
[[package]] [[package]]
name = "android-activity" name = "android-activity"
@ -1382,9 +1382,9 @@ dependencies = [
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.31.0" version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "gl_generator" name = "gl_generator"
@ -2403,9 +2403,9 @@ dependencies = [
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.4" version = "0.36.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View file

@ -28,7 +28,7 @@ kludgine = { git = "https://github.com/khonsulabs/kludgine", features = [
"app", "app",
] } ] }
figures = { version = "0.4.0" } figures = { version = "0.4.0" }
alot = "0.3" alot = "0.3.2"
interner = "0.2.1" interner = "0.2.1"
kempt = "0.2.1" kempt = "0.2.1"
intentional = "0.1.0" intentional = "0.1.0"

50
examples/nested-modals.rs Normal file
View file

@ -0,0 +1,50 @@
use cushy::dialog::MessageBox;
use cushy::widget::MakeWidget;
use cushy::widgets::layers::{Modal, ModalTarget};
use cushy::Run;
fn main() -> cushy::Result {
let modal = Modal::new();
"Show Modal"
.into_button()
.on_click({
let modal = modal.clone();
move |_| show_modal(&modal, 1)
})
.align_top()
.pad()
.and(modal)
.into_layers()
.run()
}
fn show_modal(present_in: &impl ModalTarget, level: usize) {
let handle = present_in.pending_handle();
handle
.build_dialog(
format!("Modal level: {level}")
.and("Go Deeper".into_button().on_click({
let handle = handle.clone();
move |_| {
show_modal(&handle, level + 1);
}
}))
.and("Show message".into_button().on_click({
let handle = handle.clone();
move |_| {
MessageBox::message("This is a MessageBox shown above a modal")
.open(&handle);
}
}))
.into_rows(),
)
.with_default_button("Close", || {})
.with_cancel_button("Close All", {
let handle = handle.clone();
move || {
handle.layer().dismiss();
}
})
.show();
}

View file

@ -307,7 +307,8 @@ impl<'context> EventContext<'context> {
} }
pub(crate) fn update_hovered_widget(&mut self) { pub(crate) fn update_hovered_widget(&mut self) {
self.cursor.widget = None; let current_hover = self.cursor.widget.take();
if let Some(location) = self.cursor.location { if let Some(location) = self.cursor.location {
for widget in self.tree.widgets_under_point(location) { for widget in self.tree.widgets_under_point(location) {
let mut widget_context = self.for_other(&widget); let mut widget_context = self.for_other(&widget);
@ -317,7 +318,9 @@ impl<'context> EventContext<'context> {
let relative = location - widget_layout.origin; let relative = location - widget_layout.origin;
if widget_context.hit_test(relative) { if widget_context.hit_test(relative) {
widget_context.hover(location); if current_hover != Some(widget.id()) {
widget_context.hover(location);
}
drop(widget_context); drop(widget_context);
self.cursor.widget = Some(widget.id()); self.cursor.widget = Some(widget.id());
break; break;

View file

@ -15,7 +15,7 @@ use crate::value::{Destination, Dynamic, Source};
use crate::widget::{MakeWidget, OnceCallback, SharedCallback, WidgetList}; use crate::widget::{MakeWidget, OnceCallback, SharedCallback, WidgetList};
use crate::widgets::button::{ButtonKind, ClickCounter}; use crate::widgets::button::{ButtonKind, ClickCounter};
use crate::widgets::input::InputValue; use crate::widgets::input::InputValue;
use crate::widgets::layers::Modal; use crate::widgets::layers::{Modal, ModalTarget};
use crate::widgets::Custom; use crate::widgets::Custom;
use crate::ModifiersExt; use crate::ModifiersExt;
@ -274,7 +274,10 @@ impl MessageBox {
/// Opens this dialog in the given target. /// Opens this dialog in the given target.
/// ///
/// A target can be a [`Modal`] layer, a [`WindowHandle`], or an [`App`]. /// A target can be a [`Modal`] layer, a
/// [`ModalHandle`](crate::widgets::layers::ModalHandle), a
/// [`WindowHandle`](crate::window::WindowHandle), or an
/// [`App`](crate::App).
pub fn open(&self, open_in: &impl OpenMessageBox) { pub fn open(&self, open_in: &impl OpenMessageBox) {
open_in.open_message_box(self); open_in.open_message_box(self);
} }
@ -294,9 +297,13 @@ fn coalesce_empty<'a>(s1: &'a str, s2: &'a str) -> &'a str {
} }
} }
impl OpenMessageBox for Modal { impl<T> OpenMessageBox for T
where
T: ModalTarget,
{
fn open_message_box(&self, message: &MessageBox) { fn open_message_box(&self, message: &MessageBox) {
let dialog = self.build_dialog( let handle = self.pending_handle();
let dialog = handle.build_dialog(
message message
.title .title
.as_str() .as_str()
@ -716,7 +723,9 @@ impl MakeWidget for FilePickerWidget {
}; };
let chosen_paths = Dynamic::<Vec<PathBuf>>::default(); let chosen_paths = Dynamic::<Vec<PathBuf>>::default();
let confirm_enabled = chosen_paths.map_each(|paths| !paths.is_empty()); let confirm_enabled = chosen_paths.map_each(move |paths| {
!paths.is_empty() && paths.iter().all(|p| p.is_file() == kind.is_file())
});
let browsing_directory = Dynamic::new( let browsing_directory = Dynamic::new(
self.picker self.picker

View file

@ -1536,11 +1536,12 @@ where
} }
impl<'a, T> DynamicMutexGuard<'a, T> { impl<'a, T> DynamicMutexGuard<'a, T> {
fn unlocked(&mut self, while_unlocked: impl FnOnce()) { fn unlocked<R>(&mut self, while_unlocked: impl FnOnce() -> R) -> R {
let previous_state = self.dynamic.during_callback_state.lock().take(); let previous_state = self.dynamic.during_callback_state.lock().take();
MutexGuard::unlocked(&mut self.guard, while_unlocked); let result = MutexGuard::unlocked(&mut self.guard, while_unlocked);
*self.dynamic.during_callback_state.lock() = previous_state; *self.dynamic.during_callback_state.lock() = previous_state;
result
} }
} }
@ -2196,7 +2197,7 @@ impl<'a, T> DynamicOrOwnedGuard<'a, T> {
} }
} }
fn unlocked(&mut self, while_unlocked: impl FnOnce()) { fn unlocked<R>(&mut self, while_unlocked: impl FnOnce() -> R) -> R {
match self { match self {
Self::Dynamic(guard) => guard.unlocked(while_unlocked), Self::Dynamic(guard) => guard.unlocked(while_unlocked),
Self::Owned(_) | Self::OwnedRef(_) => while_unlocked(), Self::Owned(_) | Self::OwnedRef(_) => while_unlocked(),
@ -2252,6 +2253,14 @@ impl<T, const READONLY: bool> DynamicGuard<'_, T, READONLY> {
pub fn prevent_notifications(&mut self) { pub fn prevent_notifications(&mut self) {
self.prevent_notifications = true; self.prevent_notifications = true;
} }
/// Executes `while_unlocked` while this guard is temporarily unlocked.
pub fn unlocked<F, R>(&mut self, while_unlocked: F) -> R
where
F: FnOnce() -> R,
{
self.guard.unlocked(while_unlocked)
}
} }
impl<'a, T, const READONLY: bool> Deref for DynamicGuard<'a, T, READONLY> { impl<'a, T, const READONLY: bool> Deref for DynamicGuard<'a, T, READONLY> {

View file

@ -8,17 +8,18 @@ use alot::{LotId, OrderedLots};
use cushy::widget::{RootBehavior, WidgetInstance}; use cushy::widget::{RootBehavior, WidgetInstance};
use easing_function::EasingFunction; use easing_function::EasingFunction;
use figures::units::{Lp, Px, UPx}; use figures::units::{Lp, Px, UPx};
use figures::{IntoComponents, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Zero}; use figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero};
use intentional::Assert; use intentional::Assert;
use super::super::widget::MountedWidget; use super::super::widget::MountedWidget;
use super::Space;
use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne}; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable};
use crate::styles::components::{EasingIn, IntrinsicPadding, ScrimColor}; use crate::styles::components::{EasingIn, ScrimColor};
use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value}; use crate::value::{Destination, Dynamic, DynamicGuard, DynamicRead, IntoValue, Source, Value};
use crate::widget::{ use crate::widget::{
Callback, MakeWidget, MountedChildren, SharedCallback, Widget, WidgetId, WidgetList, WidgetRef, Callback, MakeWidget, MakeWidgetWithTag, MountedChildren, SharedCallback, Widget, WidgetId,
WrapperWidget, WidgetList, WidgetRef, WidgetTag, WrapperWidget,
}; };
use crate::widgets::container::ContainerShadow; use crate::widgets::container::ContainerShadow;
use crate::ConstraintLimit; use crate::ConstraintLimit;
@ -892,7 +893,7 @@ impl WrapperWidget for Tooltipped {
/// Designed to be used in a [`Layers`] widget. /// Designed to be used in a [`Layers`] widget.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Modal { pub struct Modal {
modal: Dynamic<Option<WidgetInstance>>, modal: Dynamic<OrderedLots<WidgetInstance>>,
} }
impl Modal { impl Modal {
@ -906,7 +907,23 @@ impl Modal {
/// Presents `contents` as the modal session. /// Presents `contents` as the modal session.
pub fn present(&self, contents: impl MakeWidget) { pub fn present(&self, contents: impl MakeWidget) {
self.modal.set(Some(contents.make_widget())); self.present_inner(contents);
}
fn present_inner(&self, contents: impl MakeWidget) -> LotId {
let mut state = self.modal.lock();
state.push(contents.make_widget())
}
/// Returns a new pending handle that can be used to show a modal and
/// dismiss it.
#[must_use]
pub fn pending_handle(&self) -> ModalHandle {
ModalHandle {
layer: self.clone(),
above: None,
id: Dynamic::default(),
}
} }
/// Presents a modal dialog containing `message` with a default button that /// Presents a modal dialog containing `message` with a default button that
@ -919,18 +936,18 @@ impl Modal {
/// Returns a builder for a modal dialog that displays `message`. /// Returns a builder for a modal dialog that displays `message`.
pub fn build_dialog(&self, message: impl MakeWidget) -> DialogBuilder { pub fn build_dialog(&self, message: impl MakeWidget) -> DialogBuilder {
DialogBuilder::new(self, message) DialogBuilder::new(self.pending_handle(), message)
} }
/// Dismisses the modal session. /// Dismisses the modal session.
pub fn dismiss(&self) { pub fn dismiss(&self) {
self.modal.set(None); self.modal.lock().clear();
} }
/// Returns true if this layer is currently presenting a modal session. /// Returns true if this layer is currently presenting a modal session.
#[must_use] #[must_use]
pub fn visible(&self) -> bool { pub fn visible(&self) -> bool {
self.modal.map_ref(Option::is_some) !self.modal.lock().is_empty()
} }
/// Returns a function that dismisses the modal when invoked. /// Returns a function that dismisses the modal when invoked.
@ -946,67 +963,97 @@ impl Modal {
} }
} }
impl MakeWidget for Modal { impl MakeWidgetWithTag for Modal {
fn make_widget(self) -> WidgetInstance { fn make_with_tag(self, tag: WidgetTag) -> WidgetInstance {
let layer_widgets = Dynamic::default();
ModalLayer { ModalLayer {
presented: None, layers: WidgetRef::new(Layers::new(layer_widgets.clone())),
layer_widgets,
presented: Vec::new(),
focus_top_layer: false,
modal: self.modal, modal: self.modal,
} }
.make_widget() .make_with_tag(tag)
} }
} }
#[derive(Debug)] #[derive(Debug)]
struct ModalLayer { struct ModalLayer {
presented: Option<MountedWidget>, presented: Vec<WidgetInstance>,
modal: Dynamic<Option<WidgetInstance>>, layer_widgets: Dynamic<WidgetList>,
layers: WidgetRef,
modal: Dynamic<OrderedLots<WidgetInstance>>,
focus_top_layer: bool,
} }
impl Widget for ModalLayer { impl WrapperWidget for ModalLayer {
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { fn child_mut(&mut self) -> &mut WidgetRef {
if let Some(presented) = &self.presented { &mut self.layers
let bg = context.get(&ScrimColor);
context.fill(bg);
context.for_other(presented).redraw();
}
} }
fn layout( fn adjust_child_constraints(
&mut self, &mut self,
available_space: Size<ConstraintLimit>, available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>, context: &mut LayoutContext<'_, '_, '_, '_>,
) -> Size<UPx> { ) -> Size<ConstraintLimit> {
let modal = self.modal.get_tracking_invalidate(context); self.modal.invalidate_when_changed(context);
if self.presented.as_ref().map(MountedWidget::instance) != modal.as_ref() { let modal = self.modal.read();
if let Some(presented) = self.presented.take() { let mut layer_widgets = self.layer_widgets.lock();
context.remove_child(&presented); self.focus_top_layer = false;
for index in 0..modal.len().min(self.presented.len()) {
let modal_widget = &modal[index];
let presented = &mut self.presented[index];
if presented != modal_widget {
let modal_widget = modal_widget.clone();
*presented = modal_widget.clone();
layer_widgets[index * 2 + 1] = modal_widget.clone().centered().make_widget();
self.focus_top_layer = true;
} }
self.presented = modal.map(|modal| {
let mounted = context.push_child(modal);
context.for_other(&mounted).focus();
mounted
});
}
let full_area = available_space.map(ConstraintLimit::max);
if let Some(child) = &self.presented {
let padding = context.get(&IntrinsicPadding);
let layout_size = full_area - Size::squared(padding.into_upx(context.gfx.scale()));
let child_size = context
.for_other(child)
.layout(layout_size.map(ConstraintLimit::SizeToFit))
.into_signed();
let margin = full_area.into_signed() - child_size;
context.set_child_layout(
child,
Rect::new(margin.to_vec::<Point<Px>>() / 2, child_size),
);
} }
full_area for to_present in modal.iter().skip(self.presented.len()) {
self.focus_top_layer = true;
layer_widgets.push(Space::colored(context.get(&ScrimColor)));
self.presented.push(to_present.clone());
layer_widgets.push(to_present.clone().centered());
}
if self.presented.len() > modal.len() {
self.presented.truncate(modal.len());
layer_widgets.truncate(modal.len() * 2);
self.focus_top_layer = true;
}
available_space
} }
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_>) -> bool { fn position_child(
self.presented.is_some() &mut self,
size: Size<Px>,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> crate::widget::WrappedLayout {
if self.focus_top_layer {
self.focus_top_layer = false;
if let Some(mut ctx) = self
.presented
.last()
.and_then(|topmost| context.for_other(topmost))
{
ctx.focus();
}
}
Size::new(
available_space
.width
.fit_measured(size.width, context.gfx.scale()),
available_space
.height
.fit_measured(size.height, context.gfx.scale()),
)
.into()
} }
} }
@ -1020,16 +1067,16 @@ pub enum Yes {}
/// A modal dialog builder. /// A modal dialog builder.
#[must_use = "DialogBuilder::show must be called for the dialog to be shown"] #[must_use = "DialogBuilder::show must be called for the dialog to be shown"]
pub struct DialogBuilder<HasDefault = No, HasCancel = No> { pub struct DialogBuilder<HasDefault = No, HasCancel = No> {
modal: Modal, handle: ModalHandle,
message: WidgetInstance, message: WidgetInstance,
buttons: WidgetList, buttons: WidgetList,
_state: PhantomData<(HasDefault, HasCancel)>, _state: PhantomData<(HasDefault, HasCancel)>,
} }
impl DialogBuilder<No, No> { impl DialogBuilder<No, No> {
fn new(modal: &Modal, message: impl MakeWidget) -> Self { fn new(handle: ModalHandle, message: impl MakeWidget) -> Self {
Self { Self {
modal: modal.clone(), handle,
message: message.make_widget(), message: message.make_widget(),
buttons: WidgetList::new(), buttons: WidgetList::new(),
_state: PhantomData, _state: PhantomData,
@ -1065,7 +1112,7 @@ impl<HasDefault, HasCancel> DialogBuilder<HasDefault, HasCancel> {
on_click: impl FnOnce() + Send + 'static, on_click: impl FnOnce() + Send + 'static,
) { ) {
let mut on_click = Some(on_click); let mut on_click = Some(on_click);
let modal = self.modal.clone(); let modal = self.handle.clone();
let mut button = caption let mut button = caption
.into_button() .into_button()
.on_click(move |_| { .on_click(move |_| {
@ -1084,12 +1131,12 @@ impl<HasDefault, HasCancel> DialogBuilder<HasDefault, HasCancel> {
self.buttons.push(button.fit_horizontally().make_widget()); self.buttons.push(button.fit_horizontally().make_widget());
} }
/// Shows the modal dialog. /// Shows the modal dialog, returning a handle that owns the session.
pub fn show(mut self) { pub fn show(mut self) {
if self.buttons.is_empty() { if self.buttons.is_empty() {
self.inner_push_button("OK", DialogButtonKind::Default, || {}); self.inner_push_button("OK", DialogButtonKind::Default, || {});
} }
self.modal.present( self.handle.present(
self.message self.message
.and(self.buttons.into_columns().centered()) .and(self.buttons.into_columns().centered())
.into_rows() .into_rows()
@ -1108,13 +1155,13 @@ impl<HasCancel> DialogBuilder<No, HasCancel> {
) -> DialogBuilder<Yes, HasCancel> { ) -> DialogBuilder<Yes, HasCancel> {
self.inner_push_button(caption, DialogButtonKind::Default, on_click); self.inner_push_button(caption, DialogButtonKind::Default, on_click);
let Self { let Self {
modal, handle,
message, message,
buttons, buttons,
_state, _state,
} = self; } = self;
DialogBuilder { DialogBuilder {
modal, handle,
message, message,
buttons, buttons,
_state: PhantomData, _state: PhantomData,
@ -1132,13 +1179,13 @@ impl<HasDefault> DialogBuilder<HasDefault, No> {
) -> DialogBuilder<HasDefault, Yes> { ) -> DialogBuilder<HasDefault, Yes> {
self.inner_push_button(caption, DialogButtonKind::Cancel, on_click); self.inner_push_button(caption, DialogButtonKind::Cancel, on_click);
let Self { let Self {
modal, handle,
message, message,
buttons, buttons,
_state, _state,
} = self; } = self;
DialogBuilder { DialogBuilder {
modal, handle,
message, message,
buttons, buttons,
_state: PhantomData, _state: PhantomData,
@ -1152,3 +1199,100 @@ enum DialogButtonKind {
Default, Default,
Cancel, Cancel,
} }
/// A handle to a modal dialog presented in a [`Modal`] layer.
#[derive(Clone)]
pub struct ModalHandle {
layer: Modal,
above: Option<Dynamic<Option<LotId>>>,
id: Dynamic<Option<LotId>>,
}
impl ModalHandle {
fn above(mut self, other: &Self) -> Self {
self.above = Some(other.id.clone());
self
}
/// Presents `contents` as a modal dialog, updating this handle to control
/// it.
pub fn present(&self, contents: impl MakeWidget) {
let mut state = self.layer.modal.lock();
if let Some(above) = self.above.as_ref().and_then(Source::get) {
if let Some(index) = state.index_of_id(above) {
state.truncate(index + 1);
} else {
self.id.set(None);
return;
}
} else {
state.clear();
};
self.id.set(Some(state.push(contents.make_widget())));
}
// /// Prevents the modal shown by this handle from being dismissed when the
// /// last reference is dropped.
// pub fn persist(self) {
// self.id.set(None);
// drop(self);
// }
/// Dismisses the modal shown by this handle.
pub fn dismiss(&self) {
let Some(id) = self.id.take() else { return };
let mut state = self.layer.modal.lock();
let Some(index) = state.index_of_id(id) else {
return;
};
state.truncate(index);
}
/// Returns the modal layer the dialog is presented on.
#[must_use]
pub const fn layer(&self) -> &Modal {
&self.layer
}
/// Returns a builder for a modal dialog that displays `message` in a modal
/// dialog above the dialog shown by this handle.
pub fn build_dialog(&self, message: impl MakeWidget) -> DialogBuilder {
DialogBuilder::new(self.clone(), message)
}
}
impl Drop for ModalHandle {
fn drop(&mut self) {
if self.id.instances() == 1 {
self.dismiss();
}
}
}
/// A target for a [`Modal`] session.
pub trait ModalTarget: Send + 'static {
/// Returns a new handle that can be used to show a dialog above `self`.
fn pending_handle(&self) -> ModalHandle;
/// Returns a reference to the modal layer this target presents to.
fn layer(&self) -> &Modal;
}
impl ModalTarget for Modal {
fn pending_handle(&self) -> ModalHandle {
self.pending_handle()
}
fn layer(&self) -> &Modal {
self
}
}
impl ModalTarget for ModalHandle {
fn pending_handle(&self) -> ModalHandle {
self.layer.pending_handle().above(self)
}
fn layer(&self) -> &Modal {
&self.layer
}
}

View file

@ -20,7 +20,7 @@ use super::label::DynamicDisplay;
use super::{Grid, Label}; use super::{Grid, Label};
use crate::styles::{Component, RequireInvalidation}; use crate::styles::{Component, RequireInvalidation};
use crate::value::{IntoValue, MapEach, Source, Value}; use crate::value::{IntoValue, MapEach, Source, Value};
use crate::widget::{MakeWidget, WidgetInstance, WidgetList}; use crate::widget::{MakeWidget, MakeWidgetWithTag, WidgetInstance, WidgetList};
/// A list of items displayed with an optional item indicator. /// A list of items displayed with an optional item indicator.
pub struct List { pub struct List {
@ -445,8 +445,8 @@ impl ListIndicator for ListStyle {
} }
} }
impl MakeWidget for List { impl MakeWidgetWithTag for List {
fn make_widget(self) -> WidgetInstance { fn make_with_tag(self, tag: crate::widget::WidgetTag) -> WidgetInstance {
let rows = match (self.children, self.style) { let rows = match (self.children, self.style) {
(children, Value::Constant(style)) => { (children, Value::Constant(style)) => {
children.map_each(move |children| build_grid_widgets(&style, children)) children.map_each(move |children| build_grid_widgets(&style, children))
@ -459,7 +459,7 @@ impl MakeWidget for List {
Value::Dynamic(style.map_each(move |style| build_grid_widgets(style, &children))) Value::Dynamic(style.map_each(move |style| build_grid_widgets(style, &children)))
} }
}; };
Grid::from_rows(rows).make_widget() Grid::from_rows(rows).make_with_tag(tag)
} }
} }

View file

@ -4,7 +4,7 @@ use kludgine::Color;
use crate::context::{GraphicsContext, LayoutContext}; use crate::context::{GraphicsContext, LayoutContext};
use crate::styles::components::PrimaryColor; use crate::styles::components::PrimaryColor;
use crate::styles::{DynamicComponent, IntoDynamicComponentValue}; use crate::styles::{Component, DynamicComponent, IntoDynamicComponentValue};
use crate::value::{IntoValue, Value}; use crate::value::{IntoValue, Value};
use crate::widget::Widget; use crate::widget::Widget;
use crate::ConstraintLimit; use crate::ConstraintLimit;
@ -76,6 +76,24 @@ impl Widget for Space {
) -> Size<UPx> { ) -> Size<UPx> {
available_space.map(ConstraintLimit::min) available_space.map(ConstraintLimit::min)
} }
fn hit_test(
&mut self,
_location: figures::Point<figures::units::Px>,
context: &mut crate::context::EventContext<'_>,
) -> bool {
let color = match self.color.get() {
ColorSource::Color(color) => color,
ColorSource::Dynamic(dynamic_component) => {
if let Some(Component::Color(color)) = dynamic_component.resolve(context) {
color
} else {
return false;
}
}
};
color.alpha() > 0
}
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]

View file

@ -1960,6 +1960,8 @@ where
self.outer_size.set(layout_context.window().outer_size()); self.outer_size.set(layout_context.window().outer_size());
self.root.invalidate(); self.root.invalidate();
} }
layout_context.as_event_context().update_hovered_widget();
} }
fn close_requested<W>(&mut self, window: W, kludgine: &mut Kludgine) -> bool fn close_requested<W>(&mut self, window: W, kludgine: &mut Kludgine) -> bool