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,
open: Option<OpenTip>,
anchor: Rect,
stale_armed: bool,
}
struct OpenTip {
layer: LayerHandle,
scope: Option<Scope>,
truncation: Option<LayerHandle>,
}
impl TipState {
fn close(&mut self) {
self.generation += 1;
self.stale_armed = false;
if let Some(open) = self.open.take() {
open.layer.remove();
if let Some(marker) = open.truncation {
marker.remove();
}
if let Some(scope) = open.scope {
scope.dispose();
}
}
}
}
#[derive(Clone)]
pub enum TipContent {
Label(String),
Card {
size: Size,
build: Rc<dyn Fn(Scope) -> View>,
},
}
impl TipContent {
pub fn card(size: Size, build: impl Fn(Scope) -> View + 'static) -> TipContent {
TipContent::Card {
size,
build: Rc::new(build),
}
}
}
impl std::fmt::Debug for TipContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TipContent::Label(t) => f.debug_tuple("Label").field(t).finish(),
TipContent::Card { size, .. } => f.debug_struct("Card").field("size", size).finish(),
}
}
}
pub struct Tooltip;
impl Tooltip {
pub fn attach(
cx: Scope,
overlays: &Overlays,
text: impl Into<String>,
delay: Duration,
view: View,
) -> View {
Tooltip::attach_content(cx, overlays, TipContent::Label(text.into()), delay, view)
}
pub fn attach_content(
cx: Scope,
overlays: &Overlays,
content: TipContent,
delay: Duration,
view: View,
) -> View {
let text = content;
let overlays = overlays.clone();
overlays.require_pointer_motion();
let state = Rc::new(RefCell::new(TipState {
generation: 0,
open: None,
anchor: Rect::ZERO,
stale_armed: false,
}));
{
let state = state.clone();
cx.on_cleanup(move || state.borrow_mut().close());
}
let arm: Rc<dyn Fn(&mut crate::ui::EventCtx)> = {
let state = state.clone();
let overlays = overlays.clone();
let text = text.clone();
Rc::new(move |ctx: &mut crate::ui::EventCtx| {
let generation = {
let mut s = state.borrow_mut();
s.generation += 1;
s.anchor = ctx.current_rect_screen();
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.open.is_some() {
return; }
s.anchor
};
let open = show_tip(&overlays, cx, anchor, &text);
state.borrow_mut().open = open;
});
})
};
let mut view = view;
{
let state = state.clone();
let arm = arm.clone();
view.on_root(Phase::Bubble, move |ctx, ev| match ev {
UiEvent::FocusIn => arm(ctx),
UiEvent::FocusOut => state.borrow_mut().close(),
UiEvent::Key(k) if k.key == Key::Escape && k.mods == Mods::NONE => {
let open = state.borrow().open.is_some();
if open {
state.borrow_mut().close();
ctx.stop_propagation();
}
}
_ => {}
});
}
let handler = {
let state = state.clone();
move |ctx: &mut crate::ui::EventCtx, ev: &UiEvent| match ev {
UiEvent::MouseEnter => arm(ctx),
UiEvent::MouseLeave => state.borrow_mut().close(),
_ => {}
}
};
let draw_state = state.clone();
Element::new()
.style(LayoutStyle::default().align_self(crate::layout::Align::Start))
.draw(move |_canvas, rect| {
let o = crate::ui::layer_origin();
let live = rect.translate(o.x, o.y);
let generation = {
let mut s = draw_state.borrow_mut();
if s.open.is_none() || s.stale_armed || s.anchor == live {
return;
}
s.stale_armed = true;
s.generation
};
let state = draw_state.clone();
after(Duration::ZERO, move || {
let mut s = state.borrow_mut();
s.stale_armed = false;
if s.generation == generation {
s.close();
}
});
})
.on(Phase::Bubble, handler)
.child(view)
.build()
}
}
fn show_tip(overlays: &Overlays, cx: Scope, anchor: Rect, content: &TipContent) -> Option<OpenTip> {
let viewport = current_viewport();
let want = match content {
TipContent::Label(text) => Size::new(crate::text::width(text) + 2, 1),
TipContent::Card { size, .. } => *size,
};
let rect = place_panel(
viewport,
anchor,
want,
PanelWidth::Content {
min: want.w.min(3),
max: want.w,
},
);
if rect.h <= 0 || rect.w <= 0 {
return None;
}
let z = overlays.top_z() + 1;
match content {
TipContent::Label(text) => {
let tokens = ¤t_theme().tokens;
let ink = tokens.text;
let ground = tokens.surface_raised;
let text = crate::text::truncate_ellipsis(text, (rect.w - 2).max(0));
let layer = overlays.layer_draw(z, 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(OpenTip {
layer,
scope: None,
truncation: None,
})
}
TipContent::Card { build, .. } => {
let scope = cx.child();
let view = build(scope);
let layer = overlays.layer_tree(z, rect, false, scope, view);
Some(OpenTip {
layer,
scope: Some(scope),
truncation: truncation_marker(overlays, z + 1, rect, want.h),
})
}
}
}
fn truncation_marker(
overlays: &Overlays,
z: i32,
rect: Rect,
wanted_h: i32,
) -> Option<LayerHandle> {
if rect.h >= wanted_h {
return None;
}
let hidden = wanted_h - (rect.h - 1).max(0);
let tokens = ¤t_theme().tokens;
let ink = tokens.text_faint;
let ground = tokens.surface_raised;
let label = crate::text::truncate_ellipsis(&format!("… {hidden} more"), (rect.w - 2).max(0));
let row = Rect::new(rect.x, rect.bottom() - 1, rect.w, 1);
Some(overlays.layer_draw(z, row, 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), &label, &style);
}))
}
#[cfg(test)]
#[path = "anchored_owned_tests.rs"]
mod tests;