use pass_state::PerWidgetTooltipState;
use crate::{
pass_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id,
InnerResponse, Key, LayerId, Layout, Order, Pos2, Rect, Response, Sense, Ui, UiKind, Vec2,
Widget, WidgetText,
};
fn when_was_a_toolip_last_shown_id() -> Id {
Id::new("when_was_a_toolip_last_shown")
}
pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 {
let when_was_a_toolip_last_shown =
ctx.data(|d| d.get_temp::<f64>(when_was_a_toolip_last_shown_id()));
if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown {
let now = ctx.input(|i| i.time);
(now - when_was_a_toolip_last_shown) as f32
} else {
f32::INFINITY
}
}
fn remember_that_tooltip_was_shown(ctx: &Context) {
let now = ctx.input(|i| i.time);
ctx.data_mut(|data| data.insert_temp::<f64>(when_was_a_toolip_last_shown_id(), now));
}
pub fn show_tooltip<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents)
}
pub fn show_tooltip_at_pointer<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
ctx.input(|i| i.pointer.hover_pos()).map(|pointer_pos| {
let allow_placing_below = true;
let mut pointer_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0));
pointer_rect.min.x = pointer_pos.x;
if let Some(from_global) = ctx.layer_transform_from_global(parent_layer) {
pointer_rect = from_global * pointer_rect;
}
show_tooltip_at_dyn(
ctx,
parent_layer,
widget_id,
allow_placing_below,
&pointer_rect,
Box::new(add_contents),
)
})
}
pub fn show_tooltip_for<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
widget_rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> R {
let is_touch_screen = ctx.input(|i| i.any_touches());
let allow_placing_below = !is_touch_screen; show_tooltip_at_dyn(
ctx,
parent_layer,
widget_id,
allow_placing_below,
widget_rect,
Box::new(add_contents),
)
}
pub fn show_tooltip_at<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
suggested_position: Pos2,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> R {
let allow_placing_below = true;
let rect = Rect::from_center_size(suggested_position, Vec2::ZERO);
show_tooltip_at_dyn(
ctx,
parent_layer,
widget_id,
allow_placing_below,
&rect,
Box::new(add_contents),
)
}
fn show_tooltip_at_dyn<'c, R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
allow_placing_below: bool,
widget_rect: &Rect,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> R {
let mut widget_rect = *widget_rect;
if let Some(to_global) = ctx.layer_transform_to_global(parent_layer) {
widget_rect = to_global * widget_rect;
}
remember_that_tooltip_was_shown(ctx);
let mut state = ctx.pass_state_mut(|fs| {
fs.layers
.entry(parent_layer)
.or_default()
.widget_with_tooltip = Some(widget_id);
fs.tooltips
.widget_tooltips
.get(&widget_id)
.copied()
.unwrap_or(PerWidgetTooltipState {
bounding_rect: widget_rect,
tooltip_count: 0,
})
});
let tooltip_area_id = tooltip_id(widget_id, state.tooltip_count);
let expected_tooltip_size = AreaState::load(ctx, tooltip_area_id)
.and_then(|area| area.size)
.unwrap_or(vec2(64.0, 32.0));
let screen_rect = ctx.screen_rect();
let (pivot, anchor) = find_tooltip_position(
screen_rect,
state.bounding_rect,
allow_placing_below,
expected_tooltip_size,
);
let InnerResponse { inner, response } = Area::new(tooltip_area_id)
.kind(UiKind::Popup)
.order(Order::Tooltip)
.pivot(pivot)
.fixed_pos(anchor)
.default_width(ctx.style().spacing.tooltip_width)
.sense(Sense::hover()) .show(ctx, |ui| {
ui.style_mut().interaction.selectable_labels = false;
Frame::popup(&ctx.style()).show_dyn(ui, add_contents).inner
});
state.tooltip_count += 1;
state.bounding_rect = state.bounding_rect.union(response.rect);
ctx.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
inner
}
pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id {
let tooltip_count = ctx.pass_state(|fs| {
fs.tooltips
.widget_tooltips
.get(&widget_id)
.map_or(0, |state| state.tooltip_count)
});
tooltip_id(widget_id, tooltip_count)
}
pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id {
widget_id.with(tooltip_count)
}
fn find_tooltip_position(
screen_rect: Rect,
widget_rect: Rect,
allow_placing_below: bool,
tooltip_size: Vec2,
) -> (Align2, Pos2) {
let spacing = 4.0;
if allow_placing_below
&& widget_rect.bottom() + spacing + tooltip_size.y <= screen_rect.bottom()
{
return (
Align2::LEFT_TOP,
widget_rect.left_bottom() + spacing * Vec2::DOWN,
);
}
if screen_rect.top() + tooltip_size.y + spacing <= widget_rect.top() {
return (
Align2::LEFT_BOTTOM,
widget_rect.left_top() + spacing * Vec2::UP,
);
}
if widget_rect.right() + spacing + tooltip_size.x <= screen_rect.right() {
return (
Align2::LEFT_TOP,
widget_rect.right_top() + spacing * Vec2::RIGHT,
);
}
if screen_rect.left() + tooltip_size.x + spacing <= widget_rect.left() {
return (
Align2::RIGHT_TOP,
widget_rect.left_top() + spacing * Vec2::LEFT,
);
}
(Align2::LEFT_TOP, screen_rect.left_top())
}
pub fn show_tooltip_text(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
text: impl Into<WidgetText>,
) -> Option<()> {
show_tooltip(ctx, parent_layer, widget_id, |ui| {
crate::widgets::Label::new(text).ui(ui);
})
}
pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
let primary_tooltip_area_id = tooltip_id(widget_id, 0);
ctx.memory(|mem| {
mem.areas()
.visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id))
})
}
#[derive(Clone, Copy)]
pub enum PopupCloseBehavior {
CloseOnClick,
CloseOnClickOutside,
IgnoreClicks,
}
pub fn popup_below_widget<R>(
ui: &Ui,
popup_id: Id,
widget_response: &Response,
close_behavior: PopupCloseBehavior,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
popup_above_or_below_widget(
ui,
popup_id,
widget_response,
AboveOrBelow::Below,
close_behavior,
add_contents,
)
}
pub fn popup_above_or_below_widget<R>(
parent_ui: &Ui,
popup_id: Id,
widget_response: &Response,
above_or_below: AboveOrBelow,
close_behavior: PopupCloseBehavior,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
if !parent_ui.memory(|mem| mem.is_popup_open(popup_id)) {
return None;
}
let (mut pos, pivot) = match above_or_below {
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
};
if let Some(to_global) = parent_ui
.ctx()
.layer_transform_to_global(parent_ui.layer_id())
{
pos = to_global * pos;
}
let frame = Frame::popup(parent_ui.style());
let frame_margin = frame.total_margin();
let inner_width = (widget_response.rect.width() - frame_margin.sum().x).max(0.0);
parent_ui.ctx().pass_state_mut(|fs| {
fs.layers
.entry(parent_ui.layer_id())
.or_default()
.open_popups
.insert(popup_id)
});
let response = Area::new(popup_id)
.kind(UiKind::Popup)
.order(Order::Foreground)
.fixed_pos(pos)
.default_width(inner_width)
.pivot(pivot)
.show(parent_ui.ctx(), |ui| {
frame
.show(ui, |ui| {
ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
ui.set_min_width(inner_width);
add_contents(ui)
})
.inner
})
.inner
});
let should_close = match close_behavior {
PopupCloseBehavior::CloseOnClick => widget_response.clicked_elsewhere(),
PopupCloseBehavior::CloseOnClickOutside => {
widget_response.clicked_elsewhere() && response.response.clicked_elsewhere()
}
PopupCloseBehavior::IgnoreClicks => false,
};
if parent_ui.input(|i| i.key_pressed(Key::Escape)) || should_close {
parent_ui.memory_mut(|mem| mem.close_popup());
}
Some(response.inner)
}