mirror of
https://github.com/danbulant/cushy
synced 2026-07-05 11:10:34 +00:00
Nested modals
This commit is contained in:
parent
e78d1d28bf
commit
4f3ef7d9ed
11 changed files with 326 additions and 85 deletions
|
|
@ -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
16
Cargo.lock
generated
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
50
examples/nested-modals.rs
Normal 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();
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
15
src/value.rs
15
src/value.rs
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue