mirror of
https://github.com/danbulant/cushy
synced 2026-07-05 19:20:36 +00:00
Modal layer
This commit is contained in:
parent
71f699ccda
commit
8d082ab77f
5 changed files with 138 additions and 5 deletions
|
|
@ -138,6 +138,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- `WindowHandle::execute` executes a function on the window's thread providing
|
- `WindowHandle::execute` executes a function on the window's thread providing
|
||||||
access to an `EventContext`. This can be used to gain access to the window
|
access to an `EventContext`. This can be used to gain access to the window
|
||||||
directly, including getting a reference to the underlying winit Window.
|
directly, including getting a reference to the underlying winit Window.
|
||||||
|
- `Modal` is a new layer widget that presents a single widget as a modal
|
||||||
|
session.
|
||||||
|
|
||||||
|
|
||||||
[139]: https://github.com/khonsulabs/cushy/issues/139
|
[139]: https://github.com/khonsulabs/cushy/issues/139
|
||||||
|
|
|
||||||
30
examples/modal.rs
Normal file
30
examples/modal.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
use cushy::widget::MakeWidget;
|
||||||
|
use cushy::widgets::layers::Modal;
|
||||||
|
use cushy::Run;
|
||||||
|
|
||||||
|
fn main() -> cushy::Result {
|
||||||
|
let modal = Modal::new();
|
||||||
|
|
||||||
|
"Show Modal"
|
||||||
|
.into_button()
|
||||||
|
.on_click({
|
||||||
|
let modal = modal.clone();
|
||||||
|
move |_| {
|
||||||
|
modal.present(dialog(&modal));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.align_top()
|
||||||
|
.and(modal)
|
||||||
|
.into_layers()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dialog(modal: &Modal) -> impl MakeWidget {
|
||||||
|
let modal = modal.clone();
|
||||||
|
"This is a modal"
|
||||||
|
.and("Dismiss".into_button().on_click(move |_| {
|
||||||
|
modal.dismiss();
|
||||||
|
}))
|
||||||
|
.into_rows()
|
||||||
|
.contain()
|
||||||
|
}
|
||||||
|
|
@ -243,6 +243,8 @@ define_components! {
|
||||||
/// A [`Color`] to be used as a background color for widgets that render an
|
/// A [`Color`] to be used as a background color for widgets that render an
|
||||||
/// opaque background.
|
/// opaque background.
|
||||||
OpaqueWidgetColor(Color, "opaque_color", .surface.opaque_widget)
|
OpaqueWidgetColor(Color, "opaque_color", .surface.opaque_widget)
|
||||||
|
/// A [`Color`] to be use for the transparent surface behind an overlay.
|
||||||
|
ScrimColor(Color, "scrim_color", |context| context.theme_pair().scrim.with_alpha(50))
|
||||||
/// A set of radius descriptions for how much roundness to apply to the
|
/// A set of radius descriptions for how much roundness to apply to the
|
||||||
/// shapes of widgets.
|
/// shapes of widgets.
|
||||||
CornerRadius(CornerRadii<Dimension>, "corner_radius", CornerRadii::from(Dimension::Lp(Lp::points(6))))
|
CornerRadius(CornerRadii<Dimension>, "corner_radius", CornerRadii::from(Dimension::Lp(Lp::points(6))))
|
||||||
|
|
|
||||||
|
|
@ -2575,7 +2575,7 @@ impl WidgetRef {
|
||||||
self.mounted_for_context(context).clone()
|
self.mounted_for_context(context).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns this child, mounting it in the process if necessary.
|
/// Returns this child, if it has been mounted.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn as_mounted(&self, context: &WidgetContext<'_>) -> Option<&MountedWidget> {
|
pub fn as_mounted(&self, context: &WidgetContext<'_>) -> Option<&MountedWidget> {
|
||||||
self.mounted.get(context)
|
self.mounted.get(context)
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,17 @@ 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::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero};
|
use figures::{IntoComponents, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Zero};
|
||||||
use intentional::Assert;
|
use intentional::Assert;
|
||||||
|
|
||||||
|
use super::super::widget::MountedWidget;
|
||||||
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;
|
use crate::styles::components::{EasingIn, IntrinsicPadding, ScrimColor};
|
||||||
use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value};
|
use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value};
|
||||||
use crate::widget::{
|
use crate::widget::{
|
||||||
Callback, MakeWidget, MountedChildren, MountedWidget, SharedCallback, Widget, WidgetId,
|
Callback, MakeWidget, MountedChildren, SharedCallback, Widget, WidgetId, WidgetList, WidgetRef,
|
||||||
WidgetList, WidgetRef, WrapperWidget,
|
WrapperWidget,
|
||||||
};
|
};
|
||||||
use crate::widgets::container::ContainerShadow;
|
use crate::widgets::container::ContainerShadow;
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
@ -884,3 +885,101 @@ impl WrapperWidget for Tooltipped {
|
||||||
self.data.shown_tooltip.set(None);
|
self.data.shown_tooltip.set(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A layer to present a widget in a modal session.
|
||||||
|
///
|
||||||
|
/// Designed to be used in a [`Layers`] widget.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Modal {
|
||||||
|
modal: Dynamic<Option<WidgetInstance>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Modal {
|
||||||
|
/// Returns a new modal layer.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
modal: Dynamic::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Presents `contents` as the modal session.
|
||||||
|
pub fn present(&self, contents: impl MakeWidget) {
|
||||||
|
self.modal.set(Some(contents.make_widget()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dismisses the modal session.
|
||||||
|
pub fn dismiss(&self) {
|
||||||
|
self.modal.set(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MakeWidget for Modal {
|
||||||
|
fn make_widget(self) -> WidgetInstance {
|
||||||
|
ModalLayer {
|
||||||
|
presented: None,
|
||||||
|
modal: self.modal,
|
||||||
|
}
|
||||||
|
.make_widget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ModalLayer {
|
||||||
|
presented: Option<MountedWidget>,
|
||||||
|
modal: Dynamic<Option<WidgetInstance>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&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);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_>) -> bool {
|
||||||
|
self.presented.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue