mirror of
https://github.com/danbulant/cushy
synced 2026-05-19 04:08:38 +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!
|
||||
- `WrapperWidget::activate`'s default implementation now activates the wrapped
|
||||
widget.
|
||||
- `Space` now intercepts mouse events if its color has a non-zero alpha channel.
|
||||
|
||||
### 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.
|
||||
- `Stack` no longer unwraps a `Resize` child if the resize widget is resizing in
|
||||
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
|
||||
|
||||
|
|
@ -211,6 +215,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Choosing one or more files
|
||||
- Choosing a single folder/directory
|
||||
- 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
|
||||
|
|
|
|||
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
|
|||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.1"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375"
|
||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
|
@ -75,9 +75,9 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
|||
|
||||
[[package]]
|
||||
name = "alot"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b072fc284b73a3e4154e2decdbaad711daca0e8fedfceb0d7b1cbe2dffb00e2b"
|
||||
checksum = "4c7a3dc3ad32931b2d6e97c99a702208dfd1e2c446580e5f99d1d8355df26db6"
|
||||
|
||||
[[package]]
|
||||
name = "android-activity"
|
||||
|
|
@ -1382,9 +1382,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.0"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "gl_generator"
|
||||
|
|
@ -2403,9 +2403,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.4"
|
||||
version = "0.36.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
|
||||
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ kludgine = { git = "https://github.com/khonsulabs/kludgine", features = [
|
|||
"app",
|
||||
] }
|
||||
figures = { version = "0.4.0" }
|
||||
alot = "0.3"
|
||||
alot = "0.3.2"
|
||||
interner = "0.2.1"
|
||||
kempt = "0.2.1"
|
||||
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) {
|
||||
self.cursor.widget = None;
|
||||
let current_hover = self.cursor.widget.take();
|
||||
|
||||
if let Some(location) = self.cursor.location {
|
||||
for widget in self.tree.widgets_under_point(location) {
|
||||
let mut widget_context = self.for_other(&widget);
|
||||
|
|
@ -317,7 +318,9 @@ impl<'context> EventContext<'context> {
|
|||
let relative = location - widget_layout.origin;
|
||||
|
||||
if widget_context.hit_test(relative) {
|
||||
widget_context.hover(location);
|
||||
if current_hover != Some(widget.id()) {
|
||||
widget_context.hover(location);
|
||||
}
|
||||
drop(widget_context);
|
||||
self.cursor.widget = Some(widget.id());
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use crate::value::{Destination, Dynamic, Source};
|
|||
use crate::widget::{MakeWidget, OnceCallback, SharedCallback, WidgetList};
|
||||
use crate::widgets::button::{ButtonKind, ClickCounter};
|
||||
use crate::widgets::input::InputValue;
|
||||
use crate::widgets::layers::Modal;
|
||||
use crate::widgets::layers::{Modal, ModalTarget};
|
||||
use crate::widgets::Custom;
|
||||
use crate::ModifiersExt;
|
||||
|
||||
|
|
@ -274,7 +274,10 @@ impl MessageBox {
|
|||
|
||||
/// 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) {
|
||||
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) {
|
||||
let dialog = self.build_dialog(
|
||||
let handle = self.pending_handle();
|
||||
let dialog = handle.build_dialog(
|
||||
message
|
||||
.title
|
||||
.as_str()
|
||||
|
|
@ -716,7 +723,9 @@ impl MakeWidget for FilePickerWidget {
|
|||
};
|
||||
|
||||
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(
|
||||
self.picker
|
||||
|
|
|
|||
15
src/value.rs
15
src/value.rs
|
|
@ -1536,11 +1536,12 @@ where
|
|||
}
|
||||
|
||||
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();
|
||||
MutexGuard::unlocked(&mut self.guard, while_unlocked);
|
||||
let result = MutexGuard::unlocked(&mut self.guard, while_unlocked);
|
||||
|
||||
*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 {
|
||||
Self::Dynamic(guard) => guard.unlocked(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) {
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -8,17 +8,18 @@ use alot::{LotId, OrderedLots};
|
|||
use cushy::widget::{RootBehavior, WidgetInstance};
|
||||
use easing_function::EasingFunction;
|
||||
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 super::super::widget::MountedWidget;
|
||||
use super::Space;
|
||||
use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne};
|
||||
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable};
|
||||
use crate::styles::components::{EasingIn, IntrinsicPadding, ScrimColor};
|
||||
use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value};
|
||||
use crate::styles::components::{EasingIn, ScrimColor};
|
||||
use crate::value::{Destination, Dynamic, DynamicGuard, DynamicRead, IntoValue, Source, Value};
|
||||
use crate::widget::{
|
||||
Callback, MakeWidget, MountedChildren, SharedCallback, Widget, WidgetId, WidgetList, WidgetRef,
|
||||
WrapperWidget,
|
||||
Callback, MakeWidget, MakeWidgetWithTag, MountedChildren, SharedCallback, Widget, WidgetId,
|
||||
WidgetList, WidgetRef, WidgetTag, WrapperWidget,
|
||||
};
|
||||
use crate::widgets::container::ContainerShadow;
|
||||
use crate::ConstraintLimit;
|
||||
|
|
@ -892,7 +893,7 @@ impl WrapperWidget for Tooltipped {
|
|||
/// Designed to be used in a [`Layers`] widget.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Modal {
|
||||
modal: Dynamic<Option<WidgetInstance>>,
|
||||
modal: Dynamic<OrderedLots<WidgetInstance>>,
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
|
|
@ -906,7 +907,23 @@ impl Modal {
|
|||
|
||||
/// Presents `contents` as the modal session.
|
||||
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
|
||||
|
|
@ -919,18 +936,18 @@ impl Modal {
|
|||
|
||||
/// Returns a builder for a modal dialog that displays `message`.
|
||||
pub fn build_dialog(&self, message: impl MakeWidget) -> DialogBuilder {
|
||||
DialogBuilder::new(self, message)
|
||||
DialogBuilder::new(self.pending_handle(), message)
|
||||
}
|
||||
|
||||
/// Dismisses the modal session.
|
||||
pub fn dismiss(&self) {
|
||||
self.modal.set(None);
|
||||
self.modal.lock().clear();
|
||||
}
|
||||
|
||||
/// Returns true if this layer is currently presenting a modal session.
|
||||
#[must_use]
|
||||
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.
|
||||
|
|
@ -946,67 +963,97 @@ impl Modal {
|
|||
}
|
||||
}
|
||||
|
||||
impl MakeWidget for Modal {
|
||||
fn make_widget(self) -> WidgetInstance {
|
||||
impl MakeWidgetWithTag for Modal {
|
||||
fn make_with_tag(self, tag: WidgetTag) -> WidgetInstance {
|
||||
let layer_widgets = Dynamic::default();
|
||||
|
||||
ModalLayer {
|
||||
presented: None,
|
||||
layers: WidgetRef::new(Layers::new(layer_widgets.clone())),
|
||||
layer_widgets,
|
||||
presented: Vec::new(),
|
||||
focus_top_layer: false,
|
||||
modal: self.modal,
|
||||
}
|
||||
.make_widget()
|
||||
.make_with_tag(tag)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ModalLayer {
|
||||
presented: Option<MountedWidget>,
|
||||
modal: Dynamic<Option<WidgetInstance>>,
|
||||
presented: Vec<WidgetInstance>,
|
||||
layer_widgets: Dynamic<WidgetList>,
|
||||
layers: WidgetRef,
|
||||
modal: Dynamic<OrderedLots<WidgetInstance>>,
|
||||
focus_top_layer: bool,
|
||||
}
|
||||
|
||||
impl Widget for ModalLayer {
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
|
||||
if let Some(presented) = &self.presented {
|
||||
let bg = context.get(&ScrimColor);
|
||||
context.fill(bg);
|
||||
context.for_other(presented).redraw();
|
||||
}
|
||||
impl WrapperWidget for ModalLayer {
|
||||
fn child_mut(&mut self) -> &mut WidgetRef {
|
||||
&mut self.layers
|
||||
}
|
||||
|
||||
fn layout(
|
||||
fn adjust_child_constraints(
|
||||
&mut self,
|
||||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let modal = self.modal.get_tracking_invalidate(context);
|
||||
if self.presented.as_ref().map(MountedWidget::instance) != modal.as_ref() {
|
||||
if let Some(presented) = self.presented.take() {
|
||||
context.remove_child(&presented);
|
||||
) -> Size<ConstraintLimit> {
|
||||
self.modal.invalidate_when_changed(context);
|
||||
let modal = self.modal.read();
|
||||
let mut layer_widgets = self.layer_widgets.lock();
|
||||
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 {
|
||||
self.presented.is_some()
|
||||
fn position_child(
|
||||
&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.
|
||||
#[must_use = "DialogBuilder::show must be called for the dialog to be shown"]
|
||||
pub struct DialogBuilder<HasDefault = No, HasCancel = No> {
|
||||
modal: Modal,
|
||||
handle: ModalHandle,
|
||||
message: WidgetInstance,
|
||||
buttons: WidgetList,
|
||||
_state: PhantomData<(HasDefault, HasCancel)>,
|
||||
}
|
||||
|
||||
impl DialogBuilder<No, No> {
|
||||
fn new(modal: &Modal, message: impl MakeWidget) -> Self {
|
||||
fn new(handle: ModalHandle, message: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
modal: modal.clone(),
|
||||
handle,
|
||||
message: message.make_widget(),
|
||||
buttons: WidgetList::new(),
|
||||
_state: PhantomData,
|
||||
|
|
@ -1065,7 +1112,7 @@ impl<HasDefault, HasCancel> DialogBuilder<HasDefault, HasCancel> {
|
|||
on_click: impl FnOnce() + Send + 'static,
|
||||
) {
|
||||
let mut on_click = Some(on_click);
|
||||
let modal = self.modal.clone();
|
||||
let modal = self.handle.clone();
|
||||
let mut button = caption
|
||||
.into_button()
|
||||
.on_click(move |_| {
|
||||
|
|
@ -1084,12 +1131,12 @@ impl<HasDefault, HasCancel> DialogBuilder<HasDefault, HasCancel> {
|
|||
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) {
|
||||
if self.buttons.is_empty() {
|
||||
self.inner_push_button("OK", DialogButtonKind::Default, || {});
|
||||
}
|
||||
self.modal.present(
|
||||
self.handle.present(
|
||||
self.message
|
||||
.and(self.buttons.into_columns().centered())
|
||||
.into_rows()
|
||||
|
|
@ -1108,13 +1155,13 @@ impl<HasCancel> DialogBuilder<No, HasCancel> {
|
|||
) -> DialogBuilder<Yes, HasCancel> {
|
||||
self.inner_push_button(caption, DialogButtonKind::Default, on_click);
|
||||
let Self {
|
||||
modal,
|
||||
handle,
|
||||
message,
|
||||
buttons,
|
||||
_state,
|
||||
} = self;
|
||||
DialogBuilder {
|
||||
modal,
|
||||
handle,
|
||||
message,
|
||||
buttons,
|
||||
_state: PhantomData,
|
||||
|
|
@ -1132,13 +1179,13 @@ impl<HasDefault> DialogBuilder<HasDefault, No> {
|
|||
) -> DialogBuilder<HasDefault, Yes> {
|
||||
self.inner_push_button(caption, DialogButtonKind::Cancel, on_click);
|
||||
let Self {
|
||||
modal,
|
||||
handle,
|
||||
message,
|
||||
buttons,
|
||||
_state,
|
||||
} = self;
|
||||
DialogBuilder {
|
||||
modal,
|
||||
handle,
|
||||
message,
|
||||
buttons,
|
||||
_state: PhantomData,
|
||||
|
|
@ -1152,3 +1199,100 @@ enum DialogButtonKind {
|
|||
Default,
|
||||
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 crate::styles::{Component, RequireInvalidation};
|
||||
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.
|
||||
pub struct List {
|
||||
|
|
@ -445,8 +445,8 @@ impl ListIndicator for ListStyle {
|
|||
}
|
||||
}
|
||||
|
||||
impl MakeWidget for List {
|
||||
fn make_widget(self) -> WidgetInstance {
|
||||
impl MakeWidgetWithTag for List {
|
||||
fn make_with_tag(self, tag: crate::widget::WidgetTag) -> WidgetInstance {
|
||||
let rows = match (self.children, self.style) {
|
||||
(children, Value::Constant(style)) => {
|
||||
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)))
|
||||
}
|
||||
};
|
||||
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::styles::components::PrimaryColor;
|
||||
use crate::styles::{DynamicComponent, IntoDynamicComponentValue};
|
||||
use crate::styles::{Component, DynamicComponent, IntoDynamicComponentValue};
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widget::Widget;
|
||||
use crate::ConstraintLimit;
|
||||
|
|
@ -76,6 +76,24 @@ impl Widget for Space {
|
|||
) -> Size<UPx> {
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -1960,6 +1960,8 @@ where
|
|||
self.outer_size.set(layout_context.window().outer_size());
|
||||
self.root.invalidate();
|
||||
}
|
||||
|
||||
layout_context.as_event_context().update_hovered_widget();
|
||||
}
|
||||
|
||||
fn close_requested<W>(&mut self, window: W, kludgine: &mut Kludgine) -> bool
|
||||
|
|
|
|||
Loading…
Reference in a new issue