use std::fmt;
use densa_core::{Point, Rect, Size};
use crate::ui::State;
use crate::{EventCx, PaintCx, Position, Style, Ui, UiError, Widget, WidgetId};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Anchor {
rect: Rect,
side: Side,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Side {
Below,
Beside,
}
impl Anchor {
pub fn below(rect: Rect) -> Self {
Self {
rect,
side: Side::Below,
}
}
pub fn beside(rect: Rect) -> Self {
Self {
rect,
side: Side::Beside,
}
}
pub fn at(point: Point) -> Self {
Self::below(Rect::new(point.x, point.y, 0.0, 0.0))
}
pub(crate) fn place(&self, size: Size, viewport: Size) -> Point {
let fit = |start: f32, length: f32, room: f32| start.min(room - length).max(0.0);
let rect = self.rect;
match self.side {
Side::Below => {
let below = rect.bottom();
let y = if below + size.height <= viewport.height {
below
} else if rect.top() - size.height >= 0.0 {
rect.top() - size.height
} else {
fit(below, size.height, viewport.height)
};
Point::new(fit(rect.left(), size.width, viewport.width), y)
}
Side::Beside => {
let right = rect.right();
let x = if right + size.width <= viewport.width {
right
} else if rect.left() - size.width >= 0.0 {
rect.left() - size.width
} else {
fit(right, size.width, viewport.width)
};
Point::new(x, fit(rect.top(), size.height, viewport.height))
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Popup {
pub(crate) id: WidgetId,
pub(crate) anchor: Anchor,
pub(crate) opener: Option<WidgetId>,
pub(crate) focus_before: Option<WidgetId>,
}
#[derive(Default)]
pub(crate) struct Incoming(pub(crate) Vec<(WidgetId, Box<dyn Widget>)>);
impl fmt::Debug for Incoming {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(self.0.iter().map(|(id, _)| id))
.finish()
}
}
impl State {
pub(crate) fn open_popup(
&mut self,
widget: Box<dyn Widget>,
anchor: Anchor,
style: Style,
opener: Option<WidgetId>,
) -> Result<WidgetId, UiError> {
let style = Style {
position: Position::Absolute,
..style
};
let layer = self.layer;
let end = self.nodes[layer.slot()].children.len();
let id = self.new_node(layer, end, style)?;
let focus_before = self.requested_focus.unwrap_or(self.focus);
if widget.accepts_focus() {
self.requested_focus = Some(Some(id));
}
self.popups.push(Popup {
id,
anchor,
opener,
focus_before,
});
self.incoming.0.push((id, widget));
Ok(id)
}
pub(crate) fn close_popup(&mut self, id: WidgetId) -> bool {
match self.popups.iter().position(|popup| popup.id == id) {
Some(index) => {
self.close_from(index);
true
}
None => false,
}
}
pub(crate) fn close_from(&mut self, index: usize) {
let Some(first) = self.popups.get(index).copied() else {
return;
};
let focus_inside = self.focus.is_some_and(|focus| {
self.popups[index..]
.iter()
.any(|popup| self.is_within(focus, popup.id))
});
for popup in self.popups.drain(index..).rev() {
self.closing.push(popup.id);
}
if focus_inside {
self.requested_focus = Some(first.focus_before);
}
self.needs_paint = true;
}
pub(crate) fn is_popup_open(&self, id: WidgetId) -> bool {
self.popups.iter().any(|popup| popup.id == id) && self.is_live(id)
}
pub(crate) fn popup_origin(&self, id: WidgetId, size: Size) -> Option<Point> {
let popup = self.popups.iter().find(|popup| popup.id == id)?;
Some(popup.anchor.place(size, self.viewport()))
}
}
impl Ui {
pub fn open_popup(
&mut self,
widget: impl Widget,
anchor: Anchor,
style: Style,
) -> Result<WidgetId, UiError> {
let id = self
.state
.open_popup(Box::new(widget), anchor, style, None)?;
self.settle();
Ok(id)
}
pub fn close_popup(&mut self, id: WidgetId) -> bool {
let closed = self.state.close_popup(id);
self.settle();
closed
}
pub fn close_popups(&mut self) {
self.state.close_from(0);
self.settle();
}
pub fn popups(&self) -> impl Iterator<Item = WidgetId> + '_ {
self.state.popups.iter().map(|popup| popup.id)
}
pub fn is_popup_open(&self, id: WidgetId) -> bool {
self.state.is_popup_open(id)
}
pub(crate) fn adopt(&mut self) {
let mut incoming = std::mem::take(&mut self.state.incoming.0);
for (id, widget) in incoming.drain(..) {
if !self.state.is_live(id) {
continue;
}
let slot = id.slot();
if slot >= self.widgets.len() {
self.widgets.resize_with(slot + 1, || None);
}
self.widgets[slot] = Some(widget);
}
if self.state.incoming.0.is_empty() {
self.state.incoming.0 = incoming;
}
let mut closing = std::mem::take(&mut self.state.closing);
for id in closing.drain(..) {
self.remove_widget(id);
}
if self.state.closing.is_empty() {
self.state.closing = closing;
}
}
}
impl EventCx<'_> {
pub fn open_popup(
&mut self,
widget: impl Widget,
anchor: Anchor,
style: Style,
) -> Result<WidgetId, UiError> {
let opener = Some(self.id);
self.state
.open_popup(Box::new(widget), anchor, style, opener)
}
pub fn close_popup(&mut self, id: WidgetId) -> bool {
self.state.close_popup(id)
}
pub fn close_popups(&mut self) {
self.state.close_from(0);
}
pub fn is_popup_open(&self, id: WidgetId) -> bool {
self.state.is_popup_open(id)
}
}
impl PaintCx<'_> {
pub fn is_popup_open(&self, id: WidgetId) -> bool {
self.state.is_popup_open(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
const VIEW: Size = Size {
width: 800.0,
height: 600.0,
};
#[test]
fn below_fits_flips_and_clamps() {
let size = Size::new(200.0, 100.0);
let under = Anchor::below(Rect::new(10.0, 20.0, 80.0, 30.0));
assert_eq!(under.place(size, VIEW), Point::new(10.0, 50.0));
let low = Anchor::below(Rect::new(700.0, 550.0, 80.0, 30.0));
assert_eq!(
low.place(size, VIEW),
Point::new(600.0, 450.0),
"above, moved left"
);
let tall = Anchor::below(Rect::new(0.0, 50.0, 10.0, 10.0));
assert_eq!(
tall.place(Size::new(100.0, 580.0), VIEW),
Point::new(0.0, 20.0),
"neither fits: as low as it can go"
);
}
#[test]
fn beside_fits_flips_and_clamps() {
let size = Size::new(200.0, 100.0);
let item = Anchor::beside(Rect::new(100.0, 40.0, 150.0, 24.0));
assert_eq!(item.place(size, VIEW), Point::new(250.0, 40.0));
let edge = Anchor::beside(Rect::new(500.0, 560.0, 150.0, 24.0));
assert_eq!(
edge.place(size, VIEW),
Point::new(300.0, 500.0),
"left, moved up"
);
let wide = Anchor::beside(Rect::new(100.0, 0.0, 600.0, 24.0));
assert_eq!(
wide.place(size, VIEW),
Point::new(600.0, 0.0),
"neither side: pushed in"
);
}
#[test]
fn at_a_point_is_below_an_empty_rect() {
let size = Size::new(200.0, 100.0);
assert_eq!(
Anchor::at(Point::new(790.0, 590.0)).place(size, VIEW),
Point::new(600.0, 490.0)
);
}
}