Skip to content

Commit

Permalink
Modal layer
Browse files Browse the repository at this point in the history
  • Loading branch information
ecton committed Sep 12, 2024
1 parent 71f699c commit 8d082ab
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions examples/modal.rs
Original file line number Diff line number Diff line change
@@ -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()
}
2 changes: 2 additions & 0 deletions src/styles/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))))
Expand Down
2 changes: 1 addition & 1 deletion src/widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
107 changes: 103 additions & 4 deletions src/widgets/layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
}
}

0 comments on commit 8d082ab

Please sign in to comment.