mirror of
https://github.com/danbulant/cushy
synced 2026-06-18 14:01:10 +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
|
||||
access to an `EventContext`. This can be used to gain access to the 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
|
||||
|
|
|
|||
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
|
||||
/// opaque background.
|
||||
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
|
||||
/// shapes of widgets.
|
||||
CornerRadius(CornerRadii<Dimension>, "corner_radius", CornerRadii::from(Dimension::Lp(Lp::points(6))))
|
||||
|
|
|
|||
|
|
@ -2575,7 +2575,7 @@ impl WidgetRef {
|
|||
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]
|
||||
pub fn as_mounted(&self, context: &WidgetContext<'_>) -> Option<&MountedWidget> {
|
||||
self.mounted.get(context)
|
||||
|
|
|
|||
|
|
@ -7,16 +7,17 @@ use alot::{LotId, OrderedLots};
|
|||
use cushy::widget::{RootBehavior, WidgetInstance};
|
||||
use easing_function::EasingFunction;
|
||||
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 super::super::widget::MountedWidget;
|
||||
use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne};
|
||||
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::widget::{
|
||||
Callback, MakeWidget, MountedChildren, MountedWidget, SharedCallback, Widget, WidgetId,
|
||||
WidgetList, WidgetRef, WrapperWidget,
|
||||
Callback, MakeWidget, MountedChildren, SharedCallback, Widget, WidgetId, WidgetList, WidgetRef,
|
||||
WrapperWidget,
|
||||
};
|
||||
use crate::widgets::container::ContainerShadow;
|
||||
use crate::ConstraintLimit;
|
||||
|
|
@ -884,3 +885,101 @@ impl WrapperWidget for Tooltipped {
|
|||
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