OverlayLayer

Refs #37
This commit is contained in:
Jonathan Johnson 2023-12-06 15:53:25 -08:00
parent 288119a831
commit 0d34924ddf
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
6 changed files with 625 additions and 7 deletions

65
examples/overlays.rs Normal file
View file

@ -0,0 +1,65 @@
use gooey::widget::{MakeWidget, MakeWidgetWithId, WidgetTag};
use gooey::widgets::layers::OverlayLayer;
use gooey::Run;
fn main() -> gooey::Result {
let overlay = OverlayLayer::default();
test_widget(&overlay)
.centered()
.and(overlay)
.into_layers()
.run()
}
fn test_widget(overlay: &OverlayLayer) -> impl MakeWidget {
let (my_tag, my_id) = WidgetTag::new();
let right = "Right".into_button().on_click({
let overlay = overlay.clone();
move |()| {
overlay
.build_overlay(test_widget(&overlay))
.right_of(my_id)
.show()
.forget();
}
});
let left = "Left".into_button().on_click({
let overlay = overlay.clone();
move |()| {
overlay
.build_overlay(test_widget(&overlay))
.left_of(my_id)
.show()
.forget();
}
});
let up = "Up".into_button().on_click({
let overlay = overlay.clone();
move |()| {
overlay
.build_overlay(test_widget(&overlay))
.above(my_id)
.show()
.forget();
}
});
let down = "Down".into_button().on_click({
let overlay = overlay.clone();
move |()| {
overlay
.build_overlay(test_widget(&overlay))
.below(my_id)
.show()
.forget();
}
});
up.centered()
.and(left.and(right).into_columns())
.and(down.centered())
.into_rows()
.contain()
.pad()
.make_with_id(my_tag)
}

View file

@ -937,6 +937,14 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
})
}
/// Returns true if `possible_parent` is in this widget's parent list.
#[must_use]
pub fn is_child_of(&self, possible_parent: &WidgetInstance) -> bool {
self.current_node
.tree
.is_child(self.current_node.node_id, possible_parent)
}
/// Returns true if this widget is enabled.
#[must_use]
pub const fn enabled(&self) -> bool {

View file

@ -343,6 +343,24 @@ impl Tree {
data.nodes.get(id).expect("missing widget").parent
}
pub(crate) fn is_child(&self, mut id: LotId, possible_parent: &WidgetInstance) -> bool {
let data = self.data.lock().ignore_poison();
while let Some(node) = data.nodes.get(id) {
if &node.widget == possible_parent {
return true;
}
match node.parent {
Some(parent) => {
id = parent;
}
None => break,
}
}
false
}
pub(crate) fn attach_styles(&self, id: LotId, styles: Value<Styles>) {
let mut data = self.data.lock().ignore_poison();
data.attach_styles(id, styles);

View file

@ -10,6 +10,7 @@ use std::sync::atomic::{self, AtomicU64};
use std::sync::{Arc, Mutex, MutexGuard};
use alot::LotId;
use intentional::Assert;
use kludgine::app::winit::event::{
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
};
@ -1367,6 +1368,14 @@ impl<T, R> Debug for Callback<T, R> {
}
}
impl<T, R> Eq for Callback<T, R> {}
impl<T, R> PartialEq for Callback<T, R> {
fn eq(&self, _other: &Self) -> bool {
false
}
}
impl<T, R> Callback<T, R> {
/// Returns a new instance that calls `function` each time the callback is
/// invoked.
@ -1396,6 +1405,56 @@ where
}
}
/// A function that can be invoked once with a parameter (`T`) and returns `R`.
///
/// This type is used by widgets to signal an event that can happen only onceq.
pub struct OnceCallback<T = (), R = ()>(Box<dyn OnceCallbackFunction<T, R>>);
impl<T, R> Debug for OnceCallback<T, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("OnceCallback")
.field(&(self as *const Self))
.finish()
}
}
impl<T, R> Eq for OnceCallback<T, R> {}
impl<T, R> PartialEq for OnceCallback<T, R> {
fn eq(&self, _other: &Self) -> bool {
false
}
}
impl<T, R> OnceCallback<T, R> {
/// Returns a new instance that calls `function` when the callback is
/// invoked.
pub fn new<F>(function: F) -> Self
where
F: FnOnce(T) -> R + Send + UnwindSafe + 'static,
{
Self(Box::new(Some(function)))
}
/// Invokes the wrapped function and returns the produced value.
pub fn invoke(mut self, value: T) -> R {
self.0.invoke(value)
}
}
trait OnceCallbackFunction<T, R>: Send + UnwindSafe {
fn invoke(&mut self, value: T) -> R;
}
impl<T, R, F> OnceCallbackFunction<T, R> for Option<F>
where
F: FnOnce(T) -> R + Send + UnwindSafe,
{
fn invoke(&mut self, value: T) -> R {
(self.take().assert("invoked once"))(value)
}
}
/// A [`Widget`] that has been attached to a widget hierarchy.
#[derive(Clone)]
pub struct ManagedWidget {
@ -1763,10 +1822,15 @@ impl WidgetRef {
}
/// Returns this child, mounting it in the process if necessary.
pub fn mounted(&mut self, context: &mut EventContext<'_, '_>) -> ManagedWidget {
pub fn mount_if_needed(&mut self, context: &mut EventContext<'_, '_>) {
if let WidgetRef::Unmounted(instance) = self {
*self = WidgetRef::Mounted(context.push_child(instance.clone()));
}
}
/// Returns this child, mounting it in the process if necessary.
pub fn mounted(&mut self, context: &mut EventContext<'_, '_>) -> ManagedWidget {
self.mount_if_needed(context);
let Self::Mounted(widget) = self else {
unreachable!("just initialized")
@ -1802,6 +1866,18 @@ impl Debug for WidgetRef {
}
}
impl Eq for WidgetRef {}
impl PartialEq for WidgetRef {
fn eq(&self, other: &Self) -> bool {
if let (WidgetRef::Mounted(this), WidgetRef::Mounted(other)) = (self, other) {
this == other
} else {
self.widget() == other.widget()
}
}
}
/// The unique id of a [`WidgetInstance`].
///
/// Each [`WidgetInstance`] is guaranteed to have a unique [`WidgetId`] across

View file

@ -12,7 +12,7 @@ mod expand;
pub mod grid;
pub mod input;
pub mod label;
mod layers;
pub mod layers;
mod mode_switch;
pub mod progress;
mod radio;

View file

@ -1,12 +1,18 @@
//! Widgets that stack in the Z-direction.
use std::fmt;
use alot::{LotId, OrderedLots};
use gooey::widget::{RootBehavior, WidgetInstance};
use kludgine::figures::units::UPx;
use kludgine::figures::{IntoSigned, Rect, Size, Zero};
use intentional::Assert;
use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
use crate::value::{Generation, IntoValue, Value};
use crate::widget::{Children, ManagedWidget, Widget};
use crate::value::{Dynamic, Generation, IntoValue, Value};
use crate::widget::{
Children, MakeWidget, ManagedWidget, OnceCallback, Widget, WidgetId, WidgetRef,
};
use crate::ConstraintLimit;
/// A Z-direction stack of widgets.
@ -111,7 +117,15 @@ impl Widget for Layers {
// Now we know the size of the widget, we can request the widgets fill
// the allocated space.
let layout = Rect::from(size).into_signed();
let size = Size::new(
available_space
.width
.fit_measured(size.width, context.gfx.scale()),
available_space
.height
.fit_measured(size.height, context.gfx.scale()),
);
let layout = Rect::from(size.into_signed());
for child in &self.mounted {
context
.for_other(child)
@ -151,3 +165,440 @@ impl Widget for Layers {
None
}
}
/// A widget that displays other widgets relative to widgets in another layer.
///
/// This widget is for use inside of a [`Layers`](crate::widgets::Layers)
/// widget.
#[derive(Debug, Clone, Default)]
pub struct OverlayLayer {
state: Dynamic<OverlayState>,
}
impl OverlayLayer {
/// Returns a builder for a new overlay that can be shown on this layer.
pub fn build_overlay(&self, overlay: impl MakeWidget) -> OverlayBuilder<'_> {
OverlayBuilder {
overlay: self,
layout: OverlayLayout {
widget: WidgetRef::new(overlay),
relative_to: None,
direction: Direction::Right,
requires_hover: false,
on_dismiss: None,
layout: None,
},
}
}
}
impl Widget for OverlayLayer {
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
let mut guard = self.state.lock();
let state = &mut *guard;
for child in &state.overlays {
let WidgetRef::Mounted(mounted) = &child.widget else {
continue;
};
context.for_other(mounted).redraw();
}
}
fn layout(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> {
let mut guard = self.state.lock();
let state = &mut *guard;
let available_space = available_space.map(ConstraintLimit::max);
state.process_new_overlays(&mut context.as_event_context());
for index in 0..state.overlays.len() {
let widget = state.overlays[index]
.widget
.mounted(&mut context.as_event_context());
let Some(layout) = state.overlays[index]
.layout
.or_else(|| state.layout_overlay(index, &widget, available_space, context))
else {
continue;
};
let _ignored = context
.for_other(&widget)
.layout(layout.size.into_unsigned().map(ConstraintLimit::Fill));
state.overlays[index].layout = Some(layout);
context.set_child_layout(&widget, layout);
}
drop(guard);
// Now that we're done mutating state, we can register for invalidation
// tracking.
context.invalidate_when_changed(&self.state);
// The overlay widget should never actualy impact the layout of other
// layers, despite what layouts its children are assigned. This may seem
// weird, but it would also be weird for a tooltop to expand its window
// when shown.
Size::ZERO
}
}
#[derive(Debug, Eq, PartialEq, Default)]
struct OverlayState {
overlays: OrderedLots<OverlayLayout>,
new_overlays: usize,
}
impl OverlayState {
fn process_new_overlays(&mut self, context: &mut EventContext<'_, '_>) {
while self.new_overlays > 0 {
let new_index = self.overlays.len() - self.new_overlays;
self.new_overlays -= 1;
// Determine if new_overlay is relative to an existing overlay
let new_overlay = self.overlays.get_mut_by_index(new_index).assert_expected();
new_overlay.widget.mount_if_needed(context);
let mut dismiss_from = 0;
if let Some(context) = new_overlay
.relative_to
.and_then(|id| context.for_other(&id))
{
for existing in (0..new_index).rev() {
if context.is_child_of(self.overlays[existing].widget.widget()) {
// Relative to this overlay. Dismiss any overlays
// between this and the new one.
dismiss_from = existing + 1;
break;
}
}
}
// Dismiss any overlays that are no longer going to be shown.
for index in (dismiss_from..new_index).rev() {
self.overlays.remove_by_index(index);
}
}
}
fn layout_overlay_relative(
&mut self,
index: usize,
widget: &ManagedWidget,
available_space: Size<UPx>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
relative_to: WidgetId,
) -> Option<Rect<Px>> {
// TODO resolving a widgetid should probably be easier
let direction = self.overlays[index].direction;
let relative_to = context
.widget
.for_other(&relative_to)
.map(|c| c.widget().clone())?
.last_layout()?;
let relative_to_unsigned = relative_to.into_unsigned();
let constraints = match direction {
Direction::Up => Size::new(
relative_to_unsigned.size.width,
relative_to_unsigned.origin.y,
),
Direction::Down => Size::new(
relative_to_unsigned.size.width,
available_space.height
- relative_to_unsigned.origin.y
- relative_to_unsigned.size.height,
),
Direction::Left => Size::new(
relative_to_unsigned.origin.x,
relative_to_unsigned.size.height,
),
Direction::Right => Size::new(
available_space.width.saturating_sub(
relative_to_unsigned
.origin
.x
.saturating_add(relative_to_unsigned.size.width),
),
relative_to_unsigned.size.height,
),
};
let size = context
.for_other(widget)
.layout(constraints.map(ConstraintLimit::SizeToFit))
.into_signed();
let mut layout_direction = direction;
let mut layout;
loop {
let origin = match layout_direction {
Direction::Up => Point::new(
relative_to.origin.x + relative_to.size.width / 2 - size.width / 2,
relative_to.origin.y - size.height,
),
Direction::Down => Point::new(
relative_to.origin.x + relative_to.size.width / 2 - size.width / 2,
relative_to.origin.y + relative_to.size.height,
),
Direction::Left => Point::new(
relative_to.origin.x - size.width,
relative_to.origin.y + relative_to.size.height / 2 - size.height / 2,
),
Direction::Right => Point::new(
relative_to.origin.x + relative_to.size.width,
relative_to.origin.y + relative_to.size.height / 2 - size.height / 2,
),
};
layout = Rect::new(origin.max(Point::ZERO), size);
let bottom_right = layout.extent();
if bottom_right.x > available_space.width {
layout.origin.x -= bottom_right.x - available_space.width.into_signed();
}
if bottom_right.y > available_space.height {
layout.origin.y -= bottom_right.y - available_space.height.into_signed();
}
if layout.intersects(&relative_to) || self.layout_intersects(index, &layout, context) {
layout_direction = layout_direction.next_clockwise();
if layout_direction == direction {
// No layout worked optimally.
break;
}
} else {
break;
}
}
Some(layout)
}
fn layout_intersects(
&self,
checking_index: usize,
layout: &Rect<Px>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> bool {
for index in (0..self.overlays.len()).filter(|&i| i != checking_index) {
if self.overlays[index]
.layout
.map_or(false, |check| check.intersects(layout))
{
return true;
}
}
// Verify that the the popup won't also obscure the original content.
if checking_index != 0 {
if let Some(relative_to) = self.overlays[0]
.relative_to
.and_then(|relative_to| context.widget.for_other(&relative_to))
.and_then(|c| c.widget().last_layout())
{
if relative_to.intersects(layout) {
return true;
}
}
}
false
}
fn layout_overlay(
&mut self,
index: usize,
widget: &ManagedWidget,
available_space: Size<UPx>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Option<Rect<Px>> {
if let Some(relative_to) = self.overlays[index].relative_to {
self.layout_overlay_relative(index, widget, available_space, context, relative_to)
} else {
let direction = self.overlays[index].direction;
let size = context
.for_other(widget)
.layout(available_space.map(ConstraintLimit::SizeToFit))
.into_signed();
let available_space = available_space.into_signed();
let origin = match direction {
Direction::Up => Point::new(
available_space.width / 2,
(available_space.height - size.height) / 2,
),
Direction::Down => Point::new(
available_space.width / 2,
available_space.height / 2 + size.height / 2,
),
Direction::Right => Point::new(
available_space.width / 2 + size.width / 2,
available_space.height / 2,
),
Direction::Left => Point::new(
(available_space.width - size.width) / 2,
available_space.height / 2,
),
};
Some(Rect::new(origin, size))
}
}
}
/// A builder for overlaying a widget on an [`OverlayLayer`].
pub struct OverlayBuilder<'a> {
overlay: &'a OverlayLayer,
layout: OverlayLayout,
}
impl OverlayBuilder<'_> {
/// Sets this overlay to hide automatically when it or its relative widget
/// are no longer hovered by the mouse cursor.
#[must_use]
pub fn hide_on_unhover(mut self) -> Self {
self.layout.requires_hover = true;
self
}
/// Show this overlay to the left of the specified widget.
#[must_use]
pub fn left_of(mut self, id: WidgetId) -> Self {
self.layout.relative_to = Some(id);
self.layout.direction = Direction::Left;
self
}
/// Show this overlay to the right of the specified widget.
#[must_use]
pub fn right_of(mut self, id: WidgetId) -> Self {
self.layout.relative_to = Some(id);
self.layout.direction = Direction::Right;
self
}
/// Show this overlay to show below the specified widget.
#[must_use]
pub fn below(mut self, id: WidgetId) -> Self {
self.layout.relative_to = Some(id);
self.layout.direction = Direction::Down;
self
}
/// Show this overlay to show above the specified widget.
#[must_use]
pub fn above(mut self, id: WidgetId) -> Self {
self.layout.relative_to = Some(id);
self.layout.direction = Direction::Up;
self
}
/// Sets `callback` to be invoked once this overlay is dismissed.
#[must_use]
pub fn on_dismiss(mut self, callback: OnceCallback) -> Self {
self.layout.on_dismiss = Some(callback);
self
}
/// Shows this overlay, returning a handle that to the displayed overlay.
#[must_use]
pub fn show(self) -> OverlayHandle {
self.overlay.state.map_mut(|state| {
state.new_overlays += 1;
OverlayHandle {
state: self.overlay.state.clone(),
id: state.overlays.push(self.layout),
dismiss_on_drop: true,
}
})
}
}
#[derive(Debug, Eq, PartialEq)]
struct OverlayLayout {
widget: WidgetRef,
relative_to: Option<WidgetId>,
direction: Direction,
requires_hover: bool,
layout: Option<Rect<Px>>,
on_dismiss: Option<OnceCallback>,
}
impl Drop for OverlayLayout {
fn drop(&mut self) {
if let Some(on_dismiss) = self.on_dismiss.take() {
on_dismiss.invoke(());
}
}
}
/// A relative direction.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Direction {
/// Negative along the Y axis.
Up,
/// Positive along the X axis.
Right,
/// Positive along the Y axis.
Down,
/// Legative along the X axis.
Left,
}
impl Direction {
/// Returns the next direction when rotating clockwise.
#[must_use]
pub fn next_clockwise(&self) -> Self {
match self {
Direction::Up => Direction::Right,
Direction::Down => Direction::Left,
Direction::Right => Direction::Down,
Direction::Left => Direction::Up,
}
}
}
/// A handle to an overlay that was shown in an [`OverlayLayer`].
pub struct OverlayHandle {
state: Dynamic<OverlayState>,
id: LotId,
dismiss_on_drop: bool,
}
impl OverlayHandle {
/// Dismisses this overlay and any overlays that have been displayed
/// relative to it.
pub fn dismiss(self) {
drop(self);
}
/// Drops this handle without dismissing the overlay.
pub fn forget(mut self) {
self.dismiss_on_drop = false;
drop(self);
}
}
impl Drop for OverlayHandle {
fn drop(&mut self) {
if self.dismiss_on_drop {
let mut state = self.state.lock();
let Some(index) = state.overlays.index_of_id(self.id) else {
return;
};
while state.overlays.len() > index {
let _removed = state.overlays.pop();
}
}
}
}