245 lines
7.3 KiB
Rust
245 lines
7.3 KiB
Rust
use super::{
|
|
AnyElement, ContainerStyle, Element, Flex, KeystrokeLabel, MouseEventHandler, Overlay,
|
|
OverlayFitMode, ParentElement, Text,
|
|
};
|
|
use crate::{
|
|
fonts::TextStyle,
|
|
geometry::{rect::RectF, vector::Vector2F},
|
|
json::json,
|
|
Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SizeConstraint, Task, TypeTag,
|
|
ViewContext,
|
|
};
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use std::{
|
|
borrow::Cow,
|
|
cell::{Cell, RefCell},
|
|
ops::Range,
|
|
rc::Rc,
|
|
time::Duration,
|
|
};
|
|
use util::ResultExt;
|
|
|
|
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
|
|
|
|
pub struct Tooltip<V> {
|
|
child: AnyElement<V>,
|
|
tooltip: Option<AnyElement<V>>,
|
|
_state: ElementStateHandle<Rc<TooltipState>>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct TooltipState {
|
|
visible: Cell<bool>,
|
|
position: Cell<Vector2F>,
|
|
debounce: RefCell<Option<Task<()>>>,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
|
pub struct TooltipStyle {
|
|
#[serde(flatten)]
|
|
pub container: ContainerStyle,
|
|
pub text: TextStyle,
|
|
keystroke: KeystrokeStyle,
|
|
pub max_text_width: Option<f32>,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
|
pub struct KeystrokeStyle {
|
|
#[serde(flatten)]
|
|
container: ContainerStyle,
|
|
#[serde(flatten)]
|
|
text: TextStyle,
|
|
}
|
|
|
|
impl<V: 'static> Tooltip<V> {
|
|
pub fn new<Tag: 'static>(
|
|
id: usize,
|
|
text: impl Into<Cow<'static, str>>,
|
|
action: Option<Box<dyn Action>>,
|
|
style: TooltipStyle,
|
|
child: AnyElement<V>,
|
|
cx: &mut ViewContext<V>,
|
|
) -> Self {
|
|
Self::new_dynamic(TypeTag::new::<Tag>(), id, text, action, style, child, cx)
|
|
}
|
|
|
|
pub fn new_dynamic(
|
|
mut tag: TypeTag,
|
|
id: usize,
|
|
text: impl Into<Cow<'static, str>>,
|
|
action: Option<Box<dyn Action>>,
|
|
style: TooltipStyle,
|
|
child: AnyElement<V>,
|
|
cx: &mut ViewContext<V>,
|
|
) -> Self {
|
|
tag = tag.compose(TypeTag::new::<Self>());
|
|
|
|
let focused_view_id = cx.focused_view_id();
|
|
|
|
let state_handle = cx.default_element_state_dynamic::<Rc<TooltipState>>(tag, id);
|
|
let state = state_handle.read(cx).clone();
|
|
let text = text.into();
|
|
|
|
let tooltip = if state.visible.get() {
|
|
let mut collapsed_tooltip = Self::render_tooltip(
|
|
focused_view_id,
|
|
text.clone(),
|
|
style.clone(),
|
|
action.as_ref().map(|a| a.boxed_clone()),
|
|
true,
|
|
);
|
|
Some(
|
|
Overlay::new(
|
|
Self::render_tooltip(focused_view_id, text, style, action, false)
|
|
.constrained()
|
|
.dynamically(move |constraint, view, cx| {
|
|
SizeConstraint::strict_along(
|
|
Axis::Vertical,
|
|
collapsed_tooltip.layout(constraint, view, cx).0.y(),
|
|
)
|
|
}),
|
|
)
|
|
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
|
.with_anchor_position(state.position.get())
|
|
.into_any(),
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child)
|
|
.on_hover(move |e, _, cx| {
|
|
let position = e.position;
|
|
if e.started {
|
|
if !state.visible.get() {
|
|
state.position.set(position);
|
|
|
|
let mut debounce = state.debounce.borrow_mut();
|
|
if debounce.is_none() {
|
|
*debounce = Some(cx.spawn({
|
|
let state = state.clone();
|
|
|view, mut cx| async move {
|
|
cx.background().timer(DEBOUNCE_TIMEOUT).await;
|
|
state.visible.set(true);
|
|
view.update(&mut cx, |_, cx| cx.notify()).log_err();
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
} else {
|
|
state.visible.set(false);
|
|
state.debounce.take();
|
|
cx.notify();
|
|
}
|
|
})
|
|
.into_any();
|
|
Self {
|
|
child,
|
|
tooltip,
|
|
_state: state_handle,
|
|
}
|
|
}
|
|
|
|
pub fn render_tooltip(
|
|
focused_view_id: Option<usize>,
|
|
text: impl Into<Cow<'static, str>>,
|
|
style: TooltipStyle,
|
|
action: Option<Box<dyn Action>>,
|
|
measure: bool,
|
|
) -> impl Element<V> {
|
|
Flex::row()
|
|
.with_child({
|
|
let text = if let Some(max_text_width) = style.max_text_width {
|
|
Text::new(text, style.text)
|
|
.constrained()
|
|
.with_max_width(max_text_width)
|
|
} else {
|
|
Text::new(text, style.text).constrained()
|
|
};
|
|
|
|
if measure {
|
|
text.flex(1., false).into_any()
|
|
} else {
|
|
text.flex(1., false).aligned().into_any()
|
|
}
|
|
})
|
|
.with_children(action.and_then(|action| {
|
|
let keystroke_label = KeystrokeLabel::new(
|
|
focused_view_id?,
|
|
action,
|
|
style.keystroke.container,
|
|
style.keystroke.text,
|
|
);
|
|
if measure {
|
|
Some(keystroke_label.into_any())
|
|
} else {
|
|
Some(keystroke_label.aligned().into_any())
|
|
}
|
|
}))
|
|
.contained()
|
|
.with_style(style.container)
|
|
}
|
|
}
|
|
|
|
impl<V: 'static> Element<V> for Tooltip<V> {
|
|
type LayoutState = ();
|
|
type PaintState = ();
|
|
|
|
fn layout(
|
|
&mut self,
|
|
constraint: SizeConstraint,
|
|
view: &mut V,
|
|
cx: &mut LayoutContext<V>,
|
|
) -> (Vector2F, Self::LayoutState) {
|
|
let size = self.child.layout(constraint, view, cx);
|
|
if let Some(tooltip) = self.tooltip.as_mut() {
|
|
tooltip.layout(
|
|
SizeConstraint::new(Vector2F::zero(), cx.window_size()),
|
|
view,
|
|
cx,
|
|
);
|
|
}
|
|
(size, ())
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
bounds: RectF,
|
|
visible_bounds: RectF,
|
|
_: &mut Self::LayoutState,
|
|
view: &mut V,
|
|
cx: &mut PaintContext<V>,
|
|
) {
|
|
self.child.paint(bounds.origin(), visible_bounds, view, cx);
|
|
if let Some(tooltip) = self.tooltip.as_mut() {
|
|
tooltip.paint(bounds.origin(), visible_bounds, view, cx);
|
|
}
|
|
}
|
|
|
|
fn rect_for_text_range(
|
|
&self,
|
|
range: Range<usize>,
|
|
_: RectF,
|
|
_: RectF,
|
|
_: &Self::LayoutState,
|
|
_: &Self::PaintState,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> Option<RectF> {
|
|
self.child.rect_for_text_range(range, view, cx)
|
|
}
|
|
|
|
fn debug(
|
|
&self,
|
|
_: RectF,
|
|
_: &Self::LayoutState,
|
|
_: &Self::PaintState,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> serde_json::Value {
|
|
json!({
|
|
"child": self.child.debug(view, cx),
|
|
"tooltip": self.tooltip.as_ref().map(|t| t.debug(view, cx)),
|
|
})
|
|
}
|
|
}
|