use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use crate::base::{Rect, Size};
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{after, Scope};
use crate::render::Style;
use crate::ui::{Element, Key, Mods, Phase, UiEvent, View};
use super::super::overlays::{LayerHandle, Overlays};
use super::super::theme::current_theme;
use super::super::viewport::current_viewport;
use super::{place_panel, PanelAnchor, PanelWidth};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DismissReason {
Commit,
Escape,
OutsidePress,
AnchorGone,
Resize,
}
pub(crate) fn place_owned(
viewport: Size,
anchor: Rect,
list: Size,
width: PanelWidth,
include_anchor_row: bool,
) -> Option<(Rect, bool)> {
if viewport.w <= 0 || viewport.h <= 0 {
return None;
}
if !include_anchor_row {
let rect = place_panel(viewport, anchor, list, width);
if rect.h <= 0 || rect.w <= 0 {
return None;
}
return Some((rect, rect.bottom() <= anchor.y));
}
let w = match width {
PanelWidth::MatchAnchor => anchor.w,
PanelWidth::Content { min, max } => list.w.clamp(min, max.max(min)),
}
.clamp(1, viewport.w.max(1));
let below = (viewport.h - anchor.bottom()).max(0);
let above = anchor.y.max(0);
let (flipped, rows) = if below >= list.h || below >= above {
(false, list.h.min(below))
} else {
(true, list.h.min(above))
};
if rows <= 0 {
return None;
}
let x = anchor.x.min(viewport.w - w).max(0);
let (y, h) = if flipped {
(anchor.y - rows, rows + anchor.h)
} else {
(anchor.y, anchor.h + rows)
};
Some((Rect::new(x, y, w, h), flipped))
}
struct PopupInner {
layer: Option<LayerHandle>,
scope: Option<Scope>,
on_dismiss: Option<Box<dyn FnMut(DismissReason)>>,
rect: Rect,
flipped: bool,
}
#[derive(Clone)]
pub struct Popup {
inner: Rc<RefCell<PopupInner>>,
}
impl Popup {
pub fn open(
overlays: &Overlays,
cx: Scope,
viewport: Size,
anchor: PanelAnchor,
width: PanelWidth,
list: Size,
build: impl FnOnce(Scope, bool) -> View,
) -> Option<Popup> {
Popup::open_impl(overlays, cx, viewport, anchor, width, list, false, build)
}
pub fn open_including_anchor_row(
overlays: &Overlays,
cx: Scope,
viewport: Size,
anchor: PanelAnchor,
width: PanelWidth,
list: Size,
build: impl FnOnce(Scope, bool) -> View,
) -> Option<Popup> {
Popup::open_impl(overlays, cx, viewport, anchor, width, list, true, build)
}
#[allow(clippy::too_many_arguments)] fn open_impl(
overlays: &Overlays,
cx: Scope,
viewport: Size,
anchor: PanelAnchor,
width: PanelWidth,
list: Size,
include_anchor_row: bool,
build: impl FnOnce(Scope, bool) -> View,
) -> Option<Popup> {
let (rect, flipped) = place_owned(viewport, anchor.rect, list, width, include_anchor_row)?;
let scope = cx.child();
let content = build(scope, flipped);
let popup = Popup {
inner: Rc::new(RefCell::new(PopupInner {
layer: None,
scope: Some(scope),
on_dismiss: None,
rect,
flipped,
})),
};
let wrapper = {
let p = popup.clone();
Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Percent(1.0)),
)
.on(Phase::Bubble, move |ctx, ev| {
if let UiEvent::Key(k) = ev {
if k.key == Key::Escape && k.mods == Mods::NONE {
p.dismiss(DismissReason::Escape);
ctx.stop_propagation();
}
}
})
.child(content)
.build()
};
let z = overlays.top_z() + 1;
let layer = overlays.layer_tree(z, rect, true, scope, wrapper);
{
let p = popup.clone();
overlays.on_outside_press(&layer, move || p.dismiss(DismissReason::OutsidePress));
}
popup.inner.borrow_mut().layer = Some(layer);
let weak = Rc::downgrade(&popup.inner);
scope.on_cleanup(move || {
if let Some(inner) = weak.upgrade() {
Popup { inner }.end(DismissReason::AnchorGone, false);
}
});
let weak = Rc::downgrade(&popup.inner);
let viewport_now = super::super::viewport::use_viewport(scope);
let at_open = viewport_now.get_untracked();
scope.effect_labeled("popup-resize-dismiss", move || {
if viewport_now.get() != at_open {
if let Some(inner) = weak.upgrade() {
Popup { inner }.dismiss(DismissReason::Resize);
}
}
});
Some(popup)
}
pub fn on_dismiss(&self, f: impl FnMut(DismissReason) + 'static) {
let mut inner = self.inner.borrow_mut();
if inner.layer.is_some() {
inner.on_dismiss = Some(Box::new(f));
}
}
pub fn dismiss(&self, reason: DismissReason) {
self.end(reason, true);
}
fn end(&self, reason: DismissReason, dispose_scope: bool) {
let (layer, scope, mut callback) = {
let mut inner = self.inner.borrow_mut();
let Some(layer) = inner.layer.take() else {
return; };
(layer, inner.scope.take(), inner.on_dismiss.take())
};
layer.remove();
if dispose_scope {
if let Some(scope) = scope {
scope.dispose();
}
}
if let Some(f) = callback.as_mut() {
f(reason);
}
}
pub fn close(&self) {
self.dismiss(DismissReason::Commit);
}
pub fn is_open(&self) -> bool {
self.inner.borrow().layer.is_some()
}
pub fn rect(&self) -> Rect {
self.inner.borrow().rect
}
pub fn flipped(&self) -> bool {
self.inner.borrow().flipped
}
pub fn layer(&self) -> Option<LayerHandle> {
self.inner.borrow().layer.clone()
}
}
struct TipState {
generation: u64,
layer: Option<LayerHandle>,
anchor: Rect,
}
impl TipState {
fn close(&mut self) {
self.generation += 1;
if let Some(layer) = self.layer.take() {
layer.remove();
}
}
}
pub struct Tooltip;
impl Tooltip {
pub fn attach(
cx: Scope,
overlays: &Overlays,
text: impl Into<String>,
delay: Duration,
view: View,
) -> View {
let text = text.into();
let overlays = overlays.clone();
let state = Rc::new(RefCell::new(TipState {
generation: 0,
layer: None,
anchor: Rect::ZERO,
}));
{
let state = state.clone();
cx.on_cleanup(move || state.borrow_mut().close());
}
let handler = {
let state = state.clone();
move |ctx: &mut crate::ui::EventCtx, ev: &UiEvent| match ev {
UiEvent::MouseEnter => {
let generation = {
let mut s = state.borrow_mut();
s.generation += 1;
s.anchor = ctx.current_rect();
s.generation
};
let state = state.clone();
let overlays = overlays.clone();
let text = text.clone();
after(delay, move || {
let anchor = {
let s = state.borrow();
if s.generation != generation || s.layer.is_some() {
return; }
s.anchor
};
let layer = show_tip(&overlays, anchor, &text);
state.borrow_mut().layer = layer;
});
}
UiEvent::MouseLeave => state.borrow_mut().close(),
_ => {}
}
};
Element::new()
.style(LayoutStyle::default().align_self(crate::layout::Align::Start))
.on(Phase::Bubble, handler)
.child(view)
.build()
}
}
fn show_tip(overlays: &Overlays, anchor: Rect, text: &str) -> Option<LayerHandle> {
let viewport = current_viewport();
let content = Size::new(crate::text::width(text) + 2, 1);
let rect = place_panel(
viewport,
anchor,
content,
PanelWidth::Content {
min: content.w.min(3),
max: content.w,
},
);
if rect.h <= 0 || rect.w <= 0 {
return None;
}
let tokens = ¤t_theme().tokens;
let ink = tokens.text;
let ground = tokens.surface_raised;
let text = text.to_string();
let layer = overlays.layer_draw(overlays.top_z() + 1, rect, move |canvas, rect| {
let style = Style::new().fg(ink).bg(ground);
canvas.fill_styled(rect, ' ', &style);
canvas.print_styled(crate::base::Point::new(rect.x + 1, rect.y), &text, &style);
});
Some(layer)
}
#[cfg(test)]
#[path = "anchored_owned_tests.rs"]
mod tests;