This PR reapplies #27402 which was reverted in https://github.com/zed-industries/zed/pull/30544 due to the issue @ConradIrwin reported in https://github.com/zed-industries/zed/pull/27402#issuecomment-2871745132. The reported issue is already present on main but not visible, see https://github.com/zed-industries/zed/pull/27402#issuecomment-2872546903 for more context and reproduction steps. The fix here was to move the padding for the hover popover up to the parent container. This does not fix the underlying problem but serves as workaround without any disadvantages until a better solution is found. I would currently guess that the underlying issue might be related to some rem-size calculations for small font sizes or something similar (e.g. https://github.com/zed-industries/zed/pull/22732 could possibly be somewhat related). Notably, the fix here does not cause any difference in layouting (the following screenshots are actually distinct images), yet fixes the problem at hand. ### Default font size (`15px`) | `main` | This PR | | --- | --- | |  | | ### Smaller font size (`12px`) | `main` | This PR | | --- | --- | |  |  | Furthermore, for the second scenario, the popover would be scrollable on main. As there is no scrollbar in the second image for this PR, this no longer happens with this branch. Release Notes: - N/A
3038 lines
117 KiB
Rust
3038 lines
117 KiB
Rust
//! Div is the central, reusable element that most GPUI trees will be built from.
|
|
//! It functions as a container for other elements, and provides a number of
|
|
//! useful features for laying out and styling its children as well as binding
|
|
//! mouse events and action handlers. It is meant to be similar to the HTML `<div>`
|
|
//! element, but for GPUI.
|
|
//!
|
|
//! # Build your own div
|
|
//!
|
|
//! GPUI does not directly provide APIs for stateful, multi step events like `click`
|
|
//! and `drag`. We want GPUI users to be able to build their own abstractions for
|
|
//! their own needs. However, as a UI framework, we're also obliged to provide some
|
|
//! building blocks to make the process of building your own elements easier.
|
|
//! For this we have the [`Interactivity`] and the [`StyleRefinement`] structs, as well
|
|
//! as several associated traits. Together, these provide the full suite of Dom-like events
|
|
//! and Tailwind-like styling that you can use to build your own custom elements. Div is
|
|
//! constructed by combining these two systems into an all-in-one element.
|
|
|
|
use crate::{
|
|
Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase,
|
|
Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxId,
|
|
IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, ModifiersChangedEvent,
|
|
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
|
|
Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId,
|
|
Visibility, Window, point, px, size,
|
|
};
|
|
use collections::HashMap;
|
|
use refineable::Refineable;
|
|
use smallvec::SmallVec;
|
|
use std::{
|
|
any::{Any, TypeId},
|
|
cell::RefCell,
|
|
cmp::Ordering,
|
|
fmt::Debug,
|
|
marker::PhantomData,
|
|
mem,
|
|
rc::Rc,
|
|
sync::Arc,
|
|
time::Duration,
|
|
};
|
|
use taffy::style::Overflow;
|
|
use util::ResultExt;
|
|
|
|
use super::ImageCacheProvider;
|
|
|
|
const DRAG_THRESHOLD: f64 = 2.;
|
|
const TOOLTIP_SHOW_DELAY: Duration = Duration::from_millis(500);
|
|
const HOVERABLE_TOOLTIP_HIDE_DELAY: Duration = Duration::from_millis(500);
|
|
|
|
/// The styling information for a given group.
|
|
pub struct GroupStyle {
|
|
/// The identifier for this group.
|
|
pub group: SharedString,
|
|
|
|
/// The specific style refinement that this group would apply
|
|
/// to its children.
|
|
pub style: Box<StyleRefinement>,
|
|
}
|
|
|
|
/// An event for when a drag is moving over this element, with the given state type.
|
|
pub struct DragMoveEvent<T> {
|
|
/// The mouse move event that triggered this drag move event.
|
|
pub event: MouseMoveEvent,
|
|
|
|
/// The bounds of this element.
|
|
pub bounds: Bounds<Pixels>,
|
|
drag: PhantomData<T>,
|
|
dragged_item: Arc<dyn Any>,
|
|
}
|
|
|
|
impl<T: 'static> DragMoveEvent<T> {
|
|
/// Returns the drag state for this event.
|
|
pub fn drag<'b>(&self, cx: &'b App) -> &'b T {
|
|
cx.active_drag
|
|
.as_ref()
|
|
.and_then(|drag| drag.value.downcast_ref::<T>())
|
|
.expect("DragMoveEvent is only valid when the stored active drag is of the same type.")
|
|
}
|
|
|
|
/// An item that is about to be dropped.
|
|
pub fn dragged_item(&self) -> &dyn Any {
|
|
self.dragged_item.as_ref()
|
|
}
|
|
}
|
|
|
|
impl Interactivity {
|
|
/// Bind the given callback to the mouse down event for the given mouse button, during the bubble phase
|
|
/// The imperative API equivalent of [`InteractiveElement::on_mouse_down`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to the view state from this callback.
|
|
pub fn on_mouse_down(
|
|
&mut self,
|
|
button: MouseButton,
|
|
listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.mouse_down_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Bubble
|
|
&& event.button == button
|
|
&& hitbox.is_hovered(window)
|
|
{
|
|
(listener)(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to the mouse down event for any button, during the capture phase
|
|
/// The imperative API equivalent of [`InteractiveElement::capture_any_mouse_down`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn capture_any_mouse_down(
|
|
&mut self,
|
|
listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.mouse_down_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Capture && hitbox.is_hovered(window) {
|
|
(listener)(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to the mouse down event for any button, during the bubble phase
|
|
/// the imperative API equivalent to [`InteractiveElement::on_any_mouse_down`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_any_mouse_down(
|
|
&mut self,
|
|
listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.mouse_down_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
|
(listener)(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to the mouse up event for the given button, during the bubble phase
|
|
/// the imperative API equivalent to [`InteractiveElement::on_mouse_up`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_mouse_up(
|
|
&mut self,
|
|
button: MouseButton,
|
|
listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.mouse_up_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Bubble
|
|
&& event.button == button
|
|
&& hitbox.is_hovered(window)
|
|
{
|
|
(listener)(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to the mouse up event for any button, during the capture phase
|
|
/// the imperative API equivalent to [`InteractiveElement::capture_any_mouse_up`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn capture_any_mouse_up(
|
|
&mut self,
|
|
listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.mouse_up_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Capture && hitbox.is_hovered(window) {
|
|
(listener)(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to the mouse up event for any button, during the bubble phase
|
|
/// the imperative API equivalent to [`Interactivity::on_any_mouse_up`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_any_mouse_up(
|
|
&mut self,
|
|
listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.mouse_up_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
|
(listener)(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to the mouse down event, on any button, during the capture phase,
|
|
/// when the mouse is outside of the bounds of this element.
|
|
/// The imperative API equivalent to [`InteractiveElement::on_mouse_down_out`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_mouse_down_out(
|
|
&mut self,
|
|
listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.mouse_down_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Capture && !hitbox.contains(&window.mouse_position()) {
|
|
(listener)(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to the mouse up event, for the given button, during the capture phase,
|
|
/// when the mouse is outside of the bounds of this element.
|
|
/// The imperative API equivalent to [`InteractiveElement::on_mouse_up_out`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_mouse_up_out(
|
|
&mut self,
|
|
button: MouseButton,
|
|
listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.mouse_up_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Capture
|
|
&& event.button == button
|
|
&& !hitbox.is_hovered(window)
|
|
{
|
|
(listener)(event, window, cx);
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to the mouse move event, during the bubble phase
|
|
/// The imperative API equivalent to [`InteractiveElement::on_mouse_move`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_mouse_move(
|
|
&mut self,
|
|
listener: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.mouse_move_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
|
(listener)(event, window, cx);
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to the mouse drag event of the given type. Note that this
|
|
/// will be called for all move events, inside or outside of this element, as long as the
|
|
/// drag was started with this element under the mouse. Useful for implementing draggable
|
|
/// UIs that don't conform to a drag and drop style interaction, like resizing.
|
|
/// The imperative API equivalent to [`InteractiveElement::on_drag_move`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_drag_move<T>(
|
|
&mut self,
|
|
listener: impl Fn(&DragMoveEvent<T>, &mut Window, &mut App) + 'static,
|
|
) where
|
|
T: 'static,
|
|
{
|
|
self.mouse_move_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Capture {
|
|
if let Some(drag) = &cx.active_drag {
|
|
if drag.value.as_ref().type_id() == TypeId::of::<T>() {
|
|
(listener)(
|
|
&DragMoveEvent {
|
|
event: event.clone(),
|
|
bounds: hitbox.bounds,
|
|
drag: PhantomData,
|
|
dragged_item: Arc::clone(&drag.value),
|
|
},
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to scroll wheel events during the bubble phase
|
|
/// The imperative API equivalent to [`InteractiveElement::on_scroll_wheel`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_scroll_wheel(
|
|
&mut self,
|
|
listener: impl Fn(&ScrollWheelEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.scroll_wheel_listeners
|
|
.push(Box::new(move |event, phase, hitbox, window, cx| {
|
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
|
(listener)(event, window, cx);
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to an action dispatch during the capture phase
|
|
/// The imperative API equivalent to [`InteractiveElement::capture_action`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn capture_action<A: Action>(
|
|
&mut self,
|
|
listener: impl Fn(&A, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.action_listeners.push((
|
|
TypeId::of::<A>(),
|
|
Box::new(move |action, phase, window, cx| {
|
|
let action = action.downcast_ref().unwrap();
|
|
if phase == DispatchPhase::Capture {
|
|
(listener)(action, window, cx)
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}),
|
|
));
|
|
}
|
|
|
|
/// Bind the given callback to an action dispatch during the bubble phase
|
|
/// The imperative API equivalent to [`InteractiveElement::on_action`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Window, &mut App) + 'static) {
|
|
self.action_listeners.push((
|
|
TypeId::of::<A>(),
|
|
Box::new(move |action, phase, window, cx| {
|
|
let action = action.downcast_ref().unwrap();
|
|
if phase == DispatchPhase::Bubble {
|
|
(listener)(action, window, cx)
|
|
}
|
|
}),
|
|
));
|
|
}
|
|
|
|
/// Bind the given callback to an action dispatch, based on a dynamic action parameter
|
|
/// instead of a type parameter. Useful for component libraries that want to expose
|
|
/// action bindings to their users.
|
|
/// The imperative API equivalent to [`InteractiveElement::on_boxed_action`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_boxed_action(
|
|
&mut self,
|
|
action: &dyn Action,
|
|
listener: impl Fn(&dyn Action, &mut Window, &mut App) + 'static,
|
|
) {
|
|
let action = action.boxed_clone();
|
|
self.action_listeners.push((
|
|
(*action).type_id(),
|
|
Box::new(move |_, phase, window, cx| {
|
|
if phase == DispatchPhase::Bubble {
|
|
(listener)(&*action, window, cx)
|
|
}
|
|
}),
|
|
));
|
|
}
|
|
|
|
/// Bind the given callback to key down events during the bubble phase
|
|
/// The imperative API equivalent to [`InteractiveElement::on_key_down`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_key_down(
|
|
&mut self,
|
|
listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.key_down_listeners
|
|
.push(Box::new(move |event, phase, window, cx| {
|
|
if phase == DispatchPhase::Bubble {
|
|
(listener)(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to key down events during the capture phase
|
|
/// The imperative API equivalent to [`InteractiveElement::capture_key_down`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn capture_key_down(
|
|
&mut self,
|
|
listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.key_down_listeners
|
|
.push(Box::new(move |event, phase, window, cx| {
|
|
if phase == DispatchPhase::Capture {
|
|
listener(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to key up events during the bubble phase
|
|
/// The imperative API equivalent to [`InteractiveElement::on_key_up`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static) {
|
|
self.key_up_listeners
|
|
.push(Box::new(move |event, phase, window, cx| {
|
|
if phase == DispatchPhase::Bubble {
|
|
listener(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to key up events during the capture phase
|
|
/// The imperative API equivalent to [`InteractiveElement::on_key_up`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn capture_key_up(
|
|
&mut self,
|
|
listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.key_up_listeners
|
|
.push(Box::new(move |event, phase, window, cx| {
|
|
if phase == DispatchPhase::Capture {
|
|
listener(event, window, cx)
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to modifiers changing events.
|
|
/// The imperative API equivalent to [`InteractiveElement::on_modifiers_changed`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_modifiers_changed(
|
|
&mut self,
|
|
listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static,
|
|
) {
|
|
self.modifiers_changed_listeners
|
|
.push(Box::new(move |event, window, cx| {
|
|
listener(event, window, cx)
|
|
}));
|
|
}
|
|
|
|
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element
|
|
/// The imperative API equivalent to [`InteractiveElement::on_drop`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_drop<T: 'static>(&mut self, listener: impl Fn(&T, &mut Window, &mut App) + 'static) {
|
|
self.drop_listeners.push((
|
|
TypeId::of::<T>(),
|
|
Box::new(move |dragged_value, window, cx| {
|
|
listener(dragged_value.downcast_ref().unwrap(), window, cx);
|
|
}),
|
|
));
|
|
}
|
|
|
|
/// Use the given predicate to determine whether or not a drop event should be dispatched to this element
|
|
/// The imperative API equivalent to [`InteractiveElement::can_drop`]
|
|
pub fn can_drop(
|
|
&mut self,
|
|
predicate: impl Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static,
|
|
) {
|
|
self.can_drop_predicate = Some(Box::new(predicate));
|
|
}
|
|
|
|
/// Bind the given callback to click events of this element
|
|
/// The imperative API equivalent to [`StatefulInteractiveElement::on_click`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_click(&mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static)
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.click_listeners
|
|
.push(Box::new(move |event, window, cx| {
|
|
listener(event, window, cx)
|
|
}));
|
|
}
|
|
|
|
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
|
|
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
|
|
/// the [`Self::on_drag_move`] API
|
|
/// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_drag<T, W>(
|
|
&mut self,
|
|
value: T,
|
|
constructor: impl Fn(&T, Point<Pixels>, &mut Window, &mut App) -> Entity<W> + 'static,
|
|
) where
|
|
Self: Sized,
|
|
T: 'static,
|
|
W: 'static + Render,
|
|
{
|
|
debug_assert!(
|
|
self.drag_listener.is_none(),
|
|
"calling on_drag more than once on the same element is not supported"
|
|
);
|
|
self.drag_listener = Some((
|
|
Arc::new(value),
|
|
Box::new(move |value, offset, window, cx| {
|
|
constructor(value.downcast_ref().unwrap(), offset, window, cx).into()
|
|
}),
|
|
));
|
|
}
|
|
|
|
/// Bind the given callback on the hover start and end events of this element. Note that the boolean
|
|
/// passed to the callback is true when the hover starts and false when it ends.
|
|
/// The imperative API equivalent to [`StatefulInteractiveElement::on_hover`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static)
|
|
where
|
|
Self: Sized,
|
|
{
|
|
debug_assert!(
|
|
self.hover_listener.is_none(),
|
|
"calling on_hover more than once on the same element is not supported"
|
|
);
|
|
self.hover_listener = Some(Box::new(listener));
|
|
}
|
|
|
|
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
|
/// The imperative API equivalent to [`InteractiveElement::tooltip`]
|
|
pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static)
|
|
where
|
|
Self: Sized,
|
|
{
|
|
debug_assert!(
|
|
self.tooltip_builder.is_none(),
|
|
"calling tooltip more than once on the same element is not supported"
|
|
);
|
|
self.tooltip_builder = Some(TooltipBuilder {
|
|
build: Rc::new(build_tooltip),
|
|
hoverable: false,
|
|
});
|
|
}
|
|
|
|
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
|
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
|
|
/// the tooltip. The imperative API equivalent to [`InteractiveElement::hoverable_tooltip`]
|
|
pub fn hoverable_tooltip(
|
|
&mut self,
|
|
build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
|
|
) where
|
|
Self: Sized,
|
|
{
|
|
debug_assert!(
|
|
self.tooltip_builder.is_none(),
|
|
"calling tooltip more than once on the same element is not supported"
|
|
);
|
|
self.tooltip_builder = Some(TooltipBuilder {
|
|
build: Rc::new(build_tooltip),
|
|
hoverable: true,
|
|
});
|
|
}
|
|
|
|
/// Block the mouse from interacting with this element or any of its children
|
|
/// The imperative API equivalent to [`InteractiveElement::occlude`]
|
|
pub fn occlude_mouse(&mut self) {
|
|
self.occlude_mouse = true;
|
|
}
|
|
|
|
/// Registers event handles that stop propagation of mouse events for non-scroll events.
|
|
/// The imperative API equivalent to [`InteractiveElement::block_mouse_except_scroll`]
|
|
pub fn stop_mouse_events_except_scroll(&mut self) {
|
|
self.on_any_mouse_down(|_, _, cx| cx.stop_propagation());
|
|
self.on_any_mouse_up(|_, _, cx| cx.stop_propagation());
|
|
self.on_click(|_, _, cx| cx.stop_propagation());
|
|
self.on_hover(|_, _, cx| cx.stop_propagation());
|
|
}
|
|
}
|
|
|
|
/// A trait for elements that want to use the standard GPUI event handlers that don't
|
|
/// require any state.
|
|
pub trait InteractiveElement: Sized {
|
|
/// Retrieve the interactivity state associated with this element
|
|
fn interactivity(&mut self) -> &mut Interactivity;
|
|
|
|
/// Assign this element to a group of elements that can be styled together
|
|
fn group(mut self, group: impl Into<SharedString>) -> Self {
|
|
self.interactivity().group = Some(group.into());
|
|
self
|
|
}
|
|
|
|
/// Assign this element an ID, so that it can be used with interactivity
|
|
fn id(mut self, id: impl Into<ElementId>) -> Stateful<Self> {
|
|
self.interactivity().element_id = Some(id.into());
|
|
|
|
Stateful { element: self }
|
|
}
|
|
|
|
/// Track the focus state of the given focus handle on this element.
|
|
/// If the focus handle is focused by the application, this element will
|
|
/// apply its focused styles.
|
|
fn track_focus(mut self, focus_handle: &FocusHandle) -> FocusableWrapper<Self> {
|
|
self.interactivity().focusable = true;
|
|
self.interactivity().tracked_focus_handle = Some(focus_handle.clone());
|
|
FocusableWrapper { element: self }
|
|
}
|
|
|
|
/// Set the keymap context for this element. This will be used to determine
|
|
/// which action to dispatch from the keymap.
|
|
fn key_context<C, E>(mut self, key_context: C) -> Self
|
|
where
|
|
C: TryInto<KeyContext, Error = E>,
|
|
E: Debug,
|
|
{
|
|
if let Some(key_context) = key_context.try_into().log_err() {
|
|
self.interactivity().key_context = Some(key_context);
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Apply the given style to this element when the mouse hovers over it
|
|
fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
|
|
debug_assert!(
|
|
self.interactivity().hover_style.is_none(),
|
|
"hover style already set"
|
|
);
|
|
self.interactivity().hover_style = Some(Box::new(f(StyleRefinement::default())));
|
|
self
|
|
}
|
|
|
|
/// Apply the given style to this element when the mouse hovers over a group member
|
|
fn group_hover(
|
|
mut self,
|
|
group_name: impl Into<SharedString>,
|
|
f: impl FnOnce(StyleRefinement) -> StyleRefinement,
|
|
) -> Self {
|
|
self.interactivity().group_hover_style = Some(GroupStyle {
|
|
group: group_name.into(),
|
|
style: Box::new(f(StyleRefinement::default())),
|
|
});
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to the mouse down event for the given mouse button,
|
|
/// the fluent API equivalent to [`Interactivity::on_mouse_down`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to the view state from this callback.
|
|
fn on_mouse_down(
|
|
mut self,
|
|
button: MouseButton,
|
|
listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_mouse_down(button, listener);
|
|
self
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
/// Set a key that can be used to look up this element's bounds
|
|
/// in the [`VisualTestContext::debug_bounds`] map
|
|
/// This is a noop in release builds
|
|
fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self {
|
|
self.interactivity().debug_selector = Some(f());
|
|
self
|
|
}
|
|
|
|
#[cfg(not(any(test, feature = "test-support")))]
|
|
/// Set a key that can be used to look up this element's bounds
|
|
/// in the [`VisualTestContext::debug_bounds`] map
|
|
/// This is a noop in release builds
|
|
#[inline]
|
|
fn debug_selector(self, _: impl FnOnce() -> String) -> Self {
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to the mouse down event for any button, during the capture phase
|
|
/// the fluent API equivalent to [`Interactivity::capture_any_mouse_down`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn capture_any_mouse_down(
|
|
mut self,
|
|
listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().capture_any_mouse_down(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to the mouse down event for any button, during the capture phase
|
|
/// the fluent API equivalent to [`Interactivity::on_any_mouse_down`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_any_mouse_down(
|
|
mut self,
|
|
listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_any_mouse_down(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to the mouse up event for the given button, during the bubble phase
|
|
/// the fluent API equivalent to [`Interactivity::on_mouse_up`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_mouse_up(
|
|
mut self,
|
|
button: MouseButton,
|
|
listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_mouse_up(button, listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to the mouse up event for any button, during the capture phase
|
|
/// the fluent API equivalent to [`Interactivity::capture_any_mouse_up`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn capture_any_mouse_up(
|
|
mut self,
|
|
listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().capture_any_mouse_up(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to the mouse down event, on any button, during the capture phase,
|
|
/// when the mouse is outside of the bounds of this element.
|
|
/// The fluent API equivalent to [`Interactivity::on_mouse_down_out`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_mouse_down_out(
|
|
mut self,
|
|
listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_mouse_down_out(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to the mouse up event, for the given button, during the capture phase,
|
|
/// when the mouse is outside of the bounds of this element.
|
|
/// The fluent API equivalent to [`Interactivity::on_mouse_up_out`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_mouse_up_out(
|
|
mut self,
|
|
button: MouseButton,
|
|
listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_mouse_up_out(button, listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to the mouse move event, during the bubble phase
|
|
/// The fluent API equivalent to [`Interactivity::on_mouse_move`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_mouse_move(
|
|
mut self,
|
|
listener: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_mouse_move(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to the mouse drag event of the given type. Note that this
|
|
/// will be called for all move events, inside or outside of this element, as long as the
|
|
/// drag was started with this element under the mouse. Useful for implementing draggable
|
|
/// UIs that don't conform to a drag and drop style interaction, like resizing.
|
|
/// The fluent API equivalent to [`Interactivity::on_drag_move`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_drag_move<T: 'static>(
|
|
mut self,
|
|
listener: impl Fn(&DragMoveEvent<T>, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_drag_move(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to scroll wheel events during the bubble phase
|
|
/// The fluent API equivalent to [`Interactivity::on_scroll_wheel`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_scroll_wheel(
|
|
mut self,
|
|
listener: impl Fn(&ScrollWheelEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_scroll_wheel(listener);
|
|
self
|
|
}
|
|
|
|
/// Capture the given action, before normal action dispatch can fire
|
|
/// The fluent API equivalent to [`Interactivity::on_scroll_wheel`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn capture_action<A: Action>(
|
|
mut self,
|
|
listener: impl Fn(&A, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().capture_action(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to an action dispatch during the bubble phase
|
|
/// The fluent API equivalent to [`Interactivity::on_action`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_action<A: Action>(
|
|
mut self,
|
|
listener: impl Fn(&A, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_action(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to an action dispatch, based on a dynamic action parameter
|
|
/// instead of a type parameter. Useful for component libraries that want to expose
|
|
/// action bindings to their users.
|
|
/// The fluent API equivalent to [`Interactivity::on_boxed_action`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_boxed_action(
|
|
mut self,
|
|
action: &dyn Action,
|
|
listener: impl Fn(&dyn Action, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_boxed_action(action, listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to key down events during the bubble phase
|
|
/// The fluent API equivalent to [`Interactivity::on_key_down`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_key_down(
|
|
mut self,
|
|
listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_key_down(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to key down events during the capture phase
|
|
/// The fluent API equivalent to [`Interactivity::capture_key_down`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn capture_key_down(
|
|
mut self,
|
|
listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().capture_key_down(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to key up events during the bubble phase
|
|
/// The fluent API equivalent to [`Interactivity::on_key_up`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_key_up(
|
|
mut self,
|
|
listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_key_up(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to key up events during the capture phase
|
|
/// The fluent API equivalent to [`Interactivity::capture_key_up`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn capture_key_up(
|
|
mut self,
|
|
listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().capture_key_up(listener);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to modifiers changing events.
|
|
/// The fluent API equivalent to [`Interactivity::on_modifiers_changed`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_modifiers_changed(
|
|
mut self,
|
|
listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_modifiers_changed(listener);
|
|
self
|
|
}
|
|
|
|
/// Apply the given style when the given data type is dragged over this element
|
|
fn drag_over<S: 'static>(
|
|
mut self,
|
|
f: impl 'static + Fn(StyleRefinement, &S, &Window, &App) -> StyleRefinement,
|
|
) -> Self {
|
|
self.interactivity().drag_over_styles.push((
|
|
TypeId::of::<S>(),
|
|
Box::new(move |currently_dragged: &dyn Any, window, cx| {
|
|
f(
|
|
StyleRefinement::default(),
|
|
currently_dragged.downcast_ref::<S>().unwrap(),
|
|
window,
|
|
cx,
|
|
)
|
|
}),
|
|
));
|
|
self
|
|
}
|
|
|
|
/// Apply the given style when the given data type is dragged over this element's group
|
|
fn group_drag_over<S: 'static>(
|
|
mut self,
|
|
group_name: impl Into<SharedString>,
|
|
f: impl FnOnce(StyleRefinement) -> StyleRefinement,
|
|
) -> Self {
|
|
self.interactivity().group_drag_over_styles.push((
|
|
TypeId::of::<S>(),
|
|
GroupStyle {
|
|
group: group_name.into(),
|
|
style: Box::new(f(StyleRefinement::default())),
|
|
},
|
|
));
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element
|
|
/// The fluent API equivalent to [`Interactivity::on_drop`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_drop<T: 'static>(
|
|
mut self,
|
|
listener: impl Fn(&T, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.interactivity().on_drop(listener);
|
|
self
|
|
}
|
|
|
|
/// Use the given predicate to determine whether or not a drop event should be dispatched to this element
|
|
/// The fluent API equivalent to [`Interactivity::can_drop`]
|
|
fn can_drop(
|
|
mut self,
|
|
predicate: impl Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static,
|
|
) -> Self {
|
|
self.interactivity().can_drop(predicate);
|
|
self
|
|
}
|
|
|
|
/// Block the mouse from interacting with this element or any of its children
|
|
/// The fluent API equivalent to [`Interactivity::occlude_mouse`]
|
|
fn occlude(mut self) -> Self {
|
|
self.interactivity().occlude_mouse();
|
|
self
|
|
}
|
|
|
|
/// Stops propagation of left mouse down event.
|
|
fn block_mouse_down(mut self) -> Self {
|
|
self.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
|
}
|
|
|
|
/// Registers event handles that stop propagation of mouse events for non-scroll events.
|
|
/// The fluent API equivalent to [`Interactivity::block_mouse_except_scroll`]
|
|
fn stop_mouse_events_except_scroll(mut self) -> Self {
|
|
self.interactivity().stop_mouse_events_except_scroll();
|
|
self
|
|
}
|
|
}
|
|
|
|
/// A trait for elements that want to use the standard GPUI interactivity features
|
|
/// that require state.
|
|
pub trait StatefulInteractiveElement: InteractiveElement {
|
|
/// Set this element to focusable.
|
|
fn focusable(mut self) -> FocusableWrapper<Self> {
|
|
self.interactivity().focusable = true;
|
|
FocusableWrapper { element: self }
|
|
}
|
|
|
|
/// Set the overflow x and y to scroll.
|
|
fn overflow_scroll(mut self) -> Self {
|
|
self.interactivity().base_style.overflow.x = Some(Overflow::Scroll);
|
|
self.interactivity().base_style.overflow.y = Some(Overflow::Scroll);
|
|
self
|
|
}
|
|
|
|
/// Set the overflow x to scroll.
|
|
fn overflow_x_scroll(mut self) -> Self {
|
|
self.interactivity().base_style.overflow.x = Some(Overflow::Scroll);
|
|
self
|
|
}
|
|
|
|
/// Set the overflow y to scroll.
|
|
fn overflow_y_scroll(mut self) -> Self {
|
|
self.interactivity().base_style.overflow.y = Some(Overflow::Scroll);
|
|
self
|
|
}
|
|
|
|
/// Track the scroll state of this element with the given handle.
|
|
fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
|
|
self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone());
|
|
self
|
|
}
|
|
|
|
/// Track the scroll state of this element with the given handle.
|
|
fn anchor_scroll(mut self, scroll_anchor: Option<ScrollAnchor>) -> Self {
|
|
self.interactivity().scroll_anchor = scroll_anchor;
|
|
self
|
|
}
|
|
|
|
/// Set the given styles to be applied when this element is active.
|
|
fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.interactivity().active_style = Some(Box::new(f(StyleRefinement::default())));
|
|
self
|
|
}
|
|
|
|
/// Set the given styles to be applied when this element's group is active.
|
|
fn group_active(
|
|
mut self,
|
|
group_name: impl Into<SharedString>,
|
|
f: impl FnOnce(StyleRefinement) -> StyleRefinement,
|
|
) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.interactivity().group_active_style = Some(GroupStyle {
|
|
group: group_name.into(),
|
|
style: Box::new(f(StyleRefinement::default())),
|
|
});
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback to click events of this element
|
|
/// The fluent API equivalent to [`Interactivity::on_click`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.interactivity().on_click(listener);
|
|
self
|
|
}
|
|
|
|
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
|
|
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
|
|
/// the [`Self::on_drag_move`] API.
|
|
/// The callback also has access to the offset of triggering click from the origin of parent element.
|
|
/// The fluent API equivalent to [`Interactivity::on_drag`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_drag<T, W>(
|
|
mut self,
|
|
value: T,
|
|
constructor: impl Fn(&T, Point<Pixels>, &mut Window, &mut App) -> Entity<W> + 'static,
|
|
) -> Self
|
|
where
|
|
Self: Sized,
|
|
T: 'static,
|
|
W: 'static + Render,
|
|
{
|
|
self.interactivity().on_drag(value, constructor);
|
|
self
|
|
}
|
|
|
|
/// Bind the given callback on the hover start and end events of this element. Note that the boolean
|
|
/// passed to the callback is true when the hover starts and false when it ends.
|
|
/// The fluent API equivalent to [`Interactivity::on_hover`]
|
|
///
|
|
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
|
fn on_hover(mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.interactivity().on_hover(listener);
|
|
self
|
|
}
|
|
|
|
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
|
/// The fluent API equivalent to [`Interactivity::tooltip`]
|
|
fn tooltip(mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.interactivity().tooltip(build_tooltip);
|
|
self
|
|
}
|
|
|
|
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
|
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
|
|
/// the tooltip. The fluent API equivalent to [`Interactivity::hoverable_tooltip`]
|
|
fn hoverable_tooltip(
|
|
mut self,
|
|
build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
|
|
) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.interactivity().hoverable_tooltip(build_tooltip);
|
|
self
|
|
}
|
|
}
|
|
|
|
/// A trait for providing focus related APIs to interactive elements
|
|
pub trait FocusableElement: InteractiveElement {
|
|
/// Set the given styles to be applied when this element, specifically, is focused.
|
|
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
|
|
self
|
|
}
|
|
|
|
/// Set the given styles to be applied when this element is inside another element that is focused.
|
|
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
|
|
self
|
|
}
|
|
}
|
|
|
|
pub(crate) type MouseDownListener =
|
|
Box<dyn Fn(&MouseDownEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
|
pub(crate) type MouseUpListener =
|
|
Box<dyn Fn(&MouseUpEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
|
|
|
pub(crate) type MouseMoveListener =
|
|
Box<dyn Fn(&MouseMoveEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
|
|
|
pub(crate) type ScrollWheelListener =
|
|
Box<dyn Fn(&ScrollWheelEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
|
|
|
pub(crate) type ClickListener = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
|
|
|
pub(crate) type DragListener =
|
|
Box<dyn Fn(&dyn Any, Point<Pixels>, &mut Window, &mut App) -> AnyView + 'static>;
|
|
|
|
type DropListener = Box<dyn Fn(&dyn Any, &mut Window, &mut App) + 'static>;
|
|
|
|
type CanDropPredicate = Box<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static>;
|
|
|
|
pub(crate) struct TooltipBuilder {
|
|
build: Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>,
|
|
hoverable: bool,
|
|
}
|
|
|
|
pub(crate) type KeyDownListener =
|
|
Box<dyn Fn(&KeyDownEvent, DispatchPhase, &mut Window, &mut App) + 'static>;
|
|
|
|
pub(crate) type KeyUpListener =
|
|
Box<dyn Fn(&KeyUpEvent, DispatchPhase, &mut Window, &mut App) + 'static>;
|
|
|
|
pub(crate) type ModifiersChangedListener =
|
|
Box<dyn Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static>;
|
|
|
|
pub(crate) type ActionListener =
|
|
Box<dyn Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static>;
|
|
|
|
/// Construct a new [`Div`] element
|
|
#[track_caller]
|
|
pub fn div() -> Div {
|
|
#[cfg(debug_assertions)]
|
|
let interactivity = Interactivity {
|
|
location: Some(*core::panic::Location::caller()),
|
|
..Default::default()
|
|
};
|
|
|
|
#[cfg(not(debug_assertions))]
|
|
let interactivity = Interactivity::default();
|
|
|
|
Div {
|
|
interactivity,
|
|
children: SmallVec::default(),
|
|
prepaint_listener: None,
|
|
image_cache: None,
|
|
}
|
|
}
|
|
|
|
/// A [`Div`] element, the all-in-one element for building complex UIs in GPUI
|
|
pub struct Div {
|
|
interactivity: Interactivity,
|
|
children: SmallVec<[AnyElement; 2]>,
|
|
prepaint_listener: Option<Box<dyn Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static>>,
|
|
image_cache: Option<Box<dyn ImageCacheProvider>>,
|
|
}
|
|
|
|
impl Div {
|
|
/// Add a listener to be called when the children of this `Div` are prepainted.
|
|
/// This allows you to store the [`Bounds`] of the children for later use.
|
|
pub fn on_children_prepainted(
|
|
mut self,
|
|
listener: impl Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.prepaint_listener = Some(Box::new(listener));
|
|
self
|
|
}
|
|
|
|
/// Add an image cache at the location of this div in the element tree.
|
|
pub fn image_cache(mut self, cache: impl ImageCacheProvider) -> Self {
|
|
self.image_cache = Some(Box::new(cache));
|
|
self
|
|
}
|
|
}
|
|
|
|
/// A frame state for a `Div` element, which contains layout IDs for its children.
|
|
///
|
|
/// This struct is used internally by the `Div` element to manage the layout state of its children
|
|
/// during the UI update cycle. It holds a small vector of `LayoutId` values, each corresponding to
|
|
/// a child element of the `Div`. These IDs are used to query the layout engine for the computed
|
|
/// bounds of the children after the layout phase is complete.
|
|
pub struct DivFrameState {
|
|
child_layout_ids: SmallVec<[LayoutId; 2]>,
|
|
}
|
|
|
|
impl Styled for Div {
|
|
fn style(&mut self) -> &mut StyleRefinement {
|
|
&mut self.interactivity.base_style
|
|
}
|
|
}
|
|
|
|
impl InteractiveElement for Div {
|
|
fn interactivity(&mut self) -> &mut Interactivity {
|
|
&mut self.interactivity
|
|
}
|
|
}
|
|
|
|
impl ParentElement for Div {
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.children.extend(elements)
|
|
}
|
|
}
|
|
|
|
impl Element for Div {
|
|
type RequestLayoutState = DivFrameState;
|
|
type PrepaintState = Option<Hitbox>;
|
|
|
|
fn id(&self) -> Option<ElementId> {
|
|
self.interactivity.element_id.clone()
|
|
}
|
|
|
|
fn request_layout(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> (LayoutId, Self::RequestLayoutState) {
|
|
let mut child_layout_ids = SmallVec::new();
|
|
let image_cache = self
|
|
.image_cache
|
|
.as_mut()
|
|
.map(|provider| provider.provide(window, cx));
|
|
|
|
let layout_id = window.with_image_cache(image_cache, |window| {
|
|
self.interactivity
|
|
.request_layout(global_id, window, cx, |style, window, cx| {
|
|
window.with_text_style(style.text_style().cloned(), |window| {
|
|
child_layout_ids = self
|
|
.children
|
|
.iter_mut()
|
|
.map(|child| child.request_layout(window, cx))
|
|
.collect::<SmallVec<_>>();
|
|
window.request_layout(style, child_layout_ids.iter().copied(), cx)
|
|
})
|
|
})
|
|
});
|
|
|
|
(layout_id, DivFrameState { child_layout_ids })
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
request_layout: &mut Self::RequestLayoutState,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Option<Hitbox> {
|
|
let has_prepaint_listener = self.prepaint_listener.is_some();
|
|
let mut children_bounds = Vec::with_capacity(if has_prepaint_listener {
|
|
request_layout.child_layout_ids.len()
|
|
} else {
|
|
0
|
|
});
|
|
|
|
let mut child_min = point(Pixels::MAX, Pixels::MAX);
|
|
let mut child_max = Point::default();
|
|
if let Some(handle) = self.interactivity.scroll_anchor.as_ref() {
|
|
*handle.last_origin.borrow_mut() = bounds.origin - window.element_offset();
|
|
}
|
|
let content_size = if request_layout.child_layout_ids.is_empty() {
|
|
bounds.size
|
|
} else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() {
|
|
let mut state = scroll_handle.0.borrow_mut();
|
|
state.child_bounds = Vec::with_capacity(request_layout.child_layout_ids.len());
|
|
state.bounds = bounds;
|
|
for child_layout_id in &request_layout.child_layout_ids {
|
|
let child_bounds = window.layout_bounds(*child_layout_id);
|
|
child_min = child_min.min(&child_bounds.origin);
|
|
child_max = child_max.max(&child_bounds.bottom_right());
|
|
state.child_bounds.push(child_bounds);
|
|
}
|
|
(child_max - child_min).into()
|
|
} else {
|
|
for child_layout_id in &request_layout.child_layout_ids {
|
|
let child_bounds = window.layout_bounds(*child_layout_id);
|
|
child_min = child_min.min(&child_bounds.origin);
|
|
child_max = child_max.max(&child_bounds.bottom_right());
|
|
|
|
if has_prepaint_listener {
|
|
children_bounds.push(child_bounds);
|
|
}
|
|
}
|
|
(child_max - child_min).into()
|
|
};
|
|
|
|
self.interactivity.prepaint(
|
|
global_id,
|
|
bounds,
|
|
content_size,
|
|
window,
|
|
cx,
|
|
|_style, scroll_offset, hitbox, window, cx| {
|
|
window.with_element_offset(scroll_offset, |window| {
|
|
for child in &mut self.children {
|
|
child.prepaint(window, cx);
|
|
}
|
|
});
|
|
|
|
if let Some(listener) = self.prepaint_listener.as_ref() {
|
|
listener(children_bounds, window, cx);
|
|
}
|
|
|
|
hitbox
|
|
},
|
|
)
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
_request_layout: &mut Self::RequestLayoutState,
|
|
hitbox: &mut Option<Hitbox>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
let image_cache = self
|
|
.image_cache
|
|
.as_mut()
|
|
.map(|provider| provider.provide(window, cx));
|
|
|
|
window.with_image_cache(image_cache, |window| {
|
|
self.interactivity.paint(
|
|
global_id,
|
|
bounds,
|
|
hitbox.as_ref(),
|
|
window,
|
|
cx,
|
|
|_style, window, cx| {
|
|
for child in &mut self.children {
|
|
child.paint(window, cx);
|
|
}
|
|
},
|
|
)
|
|
});
|
|
}
|
|
}
|
|
|
|
impl IntoElement for Div {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
/// The interactivity struct. Powers all of the general-purpose
|
|
/// interactivity in the `Div` element.
|
|
#[derive(Default)]
|
|
pub struct Interactivity {
|
|
/// The element ID of the element. In id is required to support a stateful subset of the interactivity such as on_click.
|
|
pub element_id: Option<ElementId>,
|
|
/// Whether the element was clicked. This will only be present after layout.
|
|
pub active: Option<bool>,
|
|
/// Whether the element was hovered. This will only be present after paint if an hitbox
|
|
/// was created for the interactive element.
|
|
pub hovered: Option<bool>,
|
|
pub(crate) tooltip_id: Option<TooltipId>,
|
|
pub(crate) content_size: Size<Pixels>,
|
|
pub(crate) key_context: Option<KeyContext>,
|
|
pub(crate) focusable: bool,
|
|
pub(crate) tracked_focus_handle: Option<FocusHandle>,
|
|
pub(crate) tracked_scroll_handle: Option<ScrollHandle>,
|
|
pub(crate) scroll_anchor: Option<ScrollAnchor>,
|
|
pub(crate) scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
|
|
pub(crate) group: Option<SharedString>,
|
|
/// The base style of the element, before any modifications are applied
|
|
/// by focus, active, etc.
|
|
pub base_style: Box<StyleRefinement>,
|
|
pub(crate) focus_style: Option<Box<StyleRefinement>>,
|
|
pub(crate) in_focus_style: Option<Box<StyleRefinement>>,
|
|
pub(crate) hover_style: Option<Box<StyleRefinement>>,
|
|
pub(crate) group_hover_style: Option<GroupStyle>,
|
|
pub(crate) active_style: Option<Box<StyleRefinement>>,
|
|
pub(crate) group_active_style: Option<GroupStyle>,
|
|
pub(crate) drag_over_styles: Vec<(
|
|
TypeId,
|
|
Box<dyn Fn(&dyn Any, &mut Window, &mut App) -> StyleRefinement>,
|
|
)>,
|
|
pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>,
|
|
pub(crate) mouse_down_listeners: Vec<MouseDownListener>,
|
|
pub(crate) mouse_up_listeners: Vec<MouseUpListener>,
|
|
pub(crate) mouse_move_listeners: Vec<MouseMoveListener>,
|
|
pub(crate) scroll_wheel_listeners: Vec<ScrollWheelListener>,
|
|
pub(crate) key_down_listeners: Vec<KeyDownListener>,
|
|
pub(crate) key_up_listeners: Vec<KeyUpListener>,
|
|
pub(crate) modifiers_changed_listeners: Vec<ModifiersChangedListener>,
|
|
pub(crate) action_listeners: Vec<(TypeId, ActionListener)>,
|
|
pub(crate) drop_listeners: Vec<(TypeId, DropListener)>,
|
|
pub(crate) can_drop_predicate: Option<CanDropPredicate>,
|
|
pub(crate) click_listeners: Vec<ClickListener>,
|
|
pub(crate) drag_listener: Option<(Arc<dyn Any>, DragListener)>,
|
|
pub(crate) hover_listener: Option<Box<dyn Fn(&bool, &mut Window, &mut App)>>,
|
|
pub(crate) tooltip_builder: Option<TooltipBuilder>,
|
|
pub(crate) occlude_mouse: bool,
|
|
|
|
#[cfg(debug_assertions)]
|
|
pub(crate) location: Option<core::panic::Location<'static>>,
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub(crate) debug_selector: Option<String>,
|
|
}
|
|
|
|
impl Interactivity {
|
|
/// Layout this element according to this interactivity state's configured styles
|
|
pub fn request_layout(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
f: impl FnOnce(Style, &mut Window, &mut App) -> LayoutId,
|
|
) -> LayoutId {
|
|
window.with_optional_element_state::<InteractiveElementState, _>(
|
|
global_id,
|
|
|element_state, window| {
|
|
let mut element_state =
|
|
element_state.map(|element_state| element_state.unwrap_or_default());
|
|
|
|
if let Some(element_state) = element_state.as_ref() {
|
|
if cx.has_active_drag() {
|
|
if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref()
|
|
{
|
|
*pending_mouse_down.borrow_mut() = None;
|
|
}
|
|
if let Some(clicked_state) = element_state.clicked_state.as_ref() {
|
|
*clicked_state.borrow_mut() = ElementClickedState::default();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure we store a focus handle in our element state if we're focusable.
|
|
// If there's an explicit focus handle we're tracking, use that. Otherwise
|
|
// create a new handle and store it in the element state, which lives for as
|
|
// as frames contain an element with this id.
|
|
if self.focusable && self.tracked_focus_handle.is_none() {
|
|
if let Some(element_state) = element_state.as_mut() {
|
|
self.tracked_focus_handle = Some(
|
|
element_state
|
|
.focus_handle
|
|
.get_or_insert_with(|| cx.focus_handle())
|
|
.clone(),
|
|
);
|
|
}
|
|
}
|
|
|
|
if let Some(scroll_handle) = self.tracked_scroll_handle.as_ref() {
|
|
self.scroll_offset = Some(scroll_handle.0.borrow().offset.clone());
|
|
} else if self.base_style.overflow.x == Some(Overflow::Scroll)
|
|
|| self.base_style.overflow.y == Some(Overflow::Scroll)
|
|
{
|
|
if let Some(element_state) = element_state.as_mut() {
|
|
self.scroll_offset = Some(
|
|
element_state
|
|
.scroll_offset
|
|
.get_or_insert_with(Rc::default)
|
|
.clone(),
|
|
);
|
|
}
|
|
}
|
|
|
|
let style = self.compute_style_internal(None, element_state.as_mut(), window, cx);
|
|
let layout_id = f(style, window, cx);
|
|
(layout_id, element_state)
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Commit the bounds of this element according to this interactivity state's configured styles.
|
|
pub fn prepaint<R>(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
content_size: Size<Pixels>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
f: impl FnOnce(&Style, Point<Pixels>, Option<Hitbox>, &mut Window, &mut App) -> R,
|
|
) -> R {
|
|
self.content_size = content_size;
|
|
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
|
window.set_focus_handle(focus_handle, cx);
|
|
}
|
|
window.with_optional_element_state::<InteractiveElementState, _>(
|
|
global_id,
|
|
|element_state, window| {
|
|
let mut element_state =
|
|
element_state.map(|element_state| element_state.unwrap_or_default());
|
|
let style = self.compute_style_internal(None, element_state.as_mut(), window, cx);
|
|
|
|
if let Some(element_state) = element_state.as_mut() {
|
|
if let Some(clicked_state) = element_state.clicked_state.as_ref() {
|
|
let clicked_state = clicked_state.borrow();
|
|
self.active = Some(clicked_state.element);
|
|
}
|
|
if let Some(active_tooltip) = element_state.active_tooltip.as_ref() {
|
|
if self.tooltip_builder.is_some() {
|
|
self.tooltip_id = set_tooltip_on_window(active_tooltip, window);
|
|
} else {
|
|
// If there is no longer a tooltip builder, remove the active tooltip.
|
|
element_state.active_tooltip.take();
|
|
}
|
|
}
|
|
}
|
|
|
|
window.with_text_style(style.text_style().cloned(), |window| {
|
|
window.with_content_mask(
|
|
style.overflow_mask(bounds, window.rem_size()),
|
|
|window| {
|
|
let hitbox = if self.should_insert_hitbox(&style) {
|
|
Some(window.insert_hitbox(bounds, self.occlude_mouse))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let scroll_offset =
|
|
self.clamp_scroll_position(bounds, &style, window, cx);
|
|
let result = f(&style, scroll_offset, hitbox, window, cx);
|
|
(result, element_state)
|
|
},
|
|
)
|
|
})
|
|
},
|
|
)
|
|
}
|
|
|
|
fn should_insert_hitbox(&self, style: &Style) -> bool {
|
|
self.occlude_mouse
|
|
|| style.mouse_cursor.is_some()
|
|
|| self.group.is_some()
|
|
|| self.scroll_offset.is_some()
|
|
|| self.tracked_focus_handle.is_some()
|
|
|| self.hover_style.is_some()
|
|
|| self.group_hover_style.is_some()
|
|
|| self.hover_listener.is_some()
|
|
|| !self.mouse_up_listeners.is_empty()
|
|
|| !self.mouse_down_listeners.is_empty()
|
|
|| !self.mouse_move_listeners.is_empty()
|
|
|| !self.click_listeners.is_empty()
|
|
|| !self.scroll_wheel_listeners.is_empty()
|
|
|| self.drag_listener.is_some()
|
|
|| !self.drop_listeners.is_empty()
|
|
|| self.tooltip_builder.is_some()
|
|
}
|
|
|
|
fn clamp_scroll_position(
|
|
&self,
|
|
bounds: Bounds<Pixels>,
|
|
style: &Style,
|
|
window: &mut Window,
|
|
_cx: &mut App,
|
|
) -> Point<Pixels> {
|
|
if let Some(scroll_offset) = self.scroll_offset.as_ref() {
|
|
let mut scroll_to_bottom = false;
|
|
let mut tracked_scroll_handle = self
|
|
.tracked_scroll_handle
|
|
.as_ref()
|
|
.map(|handle| handle.0.borrow_mut());
|
|
if let Some(mut scroll_handle_state) = tracked_scroll_handle.as_deref_mut() {
|
|
scroll_handle_state.overflow = style.overflow;
|
|
scroll_to_bottom = mem::take(&mut scroll_handle_state.scroll_to_bottom);
|
|
}
|
|
|
|
let rem_size = window.rem_size();
|
|
let padding = style.padding.to_pixels(bounds.size.into(), rem_size);
|
|
let padding_size = size(padding.left + padding.right, padding.top + padding.bottom);
|
|
let padded_content_size = self.content_size + padding_size;
|
|
let scroll_max = (padded_content_size - bounds.size).max(&Size::default());
|
|
// Clamp scroll offset in case scroll max is smaller now (e.g., if children
|
|
// were removed or the bounds became larger).
|
|
let mut scroll_offset = scroll_offset.borrow_mut();
|
|
|
|
scroll_offset.x = scroll_offset.x.clamp(-scroll_max.width, px(0.));
|
|
if scroll_to_bottom {
|
|
scroll_offset.y = -scroll_max.height;
|
|
} else {
|
|
scroll_offset.y = scroll_offset.y.clamp(-scroll_max.height, px(0.));
|
|
}
|
|
|
|
if let Some(mut scroll_handle_state) = tracked_scroll_handle {
|
|
scroll_handle_state.padded_content_size = padded_content_size;
|
|
}
|
|
|
|
*scroll_offset
|
|
} else {
|
|
Point::default()
|
|
}
|
|
}
|
|
|
|
/// Paint this element according to this interactivity state's configured styles
|
|
/// and bind the element's mouse and keyboard events.
|
|
///
|
|
/// content_size is the size of the content of the element, which may be larger than the
|
|
/// element's bounds if the element is scrollable.
|
|
///
|
|
/// the final computed style will be passed to the provided function, along
|
|
/// with the current scroll offset
|
|
pub fn paint(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
hitbox: Option<&Hitbox>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
f: impl FnOnce(&Style, &mut Window, &mut App),
|
|
) {
|
|
self.hovered = hitbox.map(|hitbox| hitbox.is_hovered(window));
|
|
window.with_optional_element_state::<InteractiveElementState, _>(
|
|
global_id,
|
|
|element_state, window| {
|
|
let mut element_state =
|
|
element_state.map(|element_state| element_state.unwrap_or_default());
|
|
|
|
let style = self.compute_style_internal(hitbox, element_state.as_mut(), window, cx);
|
|
|
|
#[cfg(any(feature = "test-support", test))]
|
|
if let Some(debug_selector) = &self.debug_selector {
|
|
window
|
|
.next_frame
|
|
.debug_bounds
|
|
.insert(debug_selector.clone(), bounds);
|
|
}
|
|
|
|
self.paint_hover_group_handler(window, cx);
|
|
|
|
if style.visibility == Visibility::Hidden {
|
|
return ((), element_state);
|
|
}
|
|
|
|
window.with_element_opacity(style.opacity, |window| {
|
|
style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| {
|
|
window.with_text_style(style.text_style().cloned(), |window| {
|
|
window.with_content_mask(
|
|
style.overflow_mask(bounds, window.rem_size()),
|
|
|window| {
|
|
if let Some(hitbox) = hitbox {
|
|
#[cfg(debug_assertions)]
|
|
self.paint_debug_info(
|
|
global_id, hitbox, &style, window, cx,
|
|
);
|
|
|
|
if let Some(drag) = cx.active_drag.as_ref() {
|
|
if let Some(mouse_cursor) = drag.cursor_style {
|
|
window.set_cursor_style(mouse_cursor, None);
|
|
}
|
|
} else {
|
|
if let Some(mouse_cursor) = style.mouse_cursor {
|
|
window.set_cursor_style(mouse_cursor, Some(hitbox));
|
|
}
|
|
}
|
|
|
|
if let Some(group) = self.group.clone() {
|
|
GroupHitboxes::push(group, hitbox.id, cx);
|
|
}
|
|
|
|
self.paint_mouse_listeners(
|
|
hitbox,
|
|
element_state.as_mut(),
|
|
window,
|
|
cx,
|
|
);
|
|
self.paint_scroll_listener(hitbox, &style, window, cx);
|
|
}
|
|
|
|
self.paint_keyboard_listeners(window, cx);
|
|
f(&style, window, cx);
|
|
|
|
if hitbox.is_some() {
|
|
if let Some(group) = self.group.as_ref() {
|
|
GroupHitboxes::pop(group, cx);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
((), element_state)
|
|
},
|
|
);
|
|
}
|
|
|
|
#[cfg(debug_assertions)]
|
|
fn paint_debug_info(
|
|
&self,
|
|
global_id: Option<&GlobalElementId>,
|
|
hitbox: &Hitbox,
|
|
style: &Style,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
use crate::{BorderStyle, TextAlign};
|
|
|
|
if global_id.is_some()
|
|
&& (style.debug || style.debug_below || cx.has_global::<crate::DebugBelow>())
|
|
&& hitbox.is_hovered(window)
|
|
{
|
|
const FONT_SIZE: crate::Pixels = crate::Pixels(10.);
|
|
let element_id = format!("{:?}", global_id.unwrap());
|
|
let str_len = element_id.len();
|
|
|
|
let render_debug_text = |window: &mut Window| {
|
|
if let Some(text) = window
|
|
.text_system()
|
|
.shape_text(
|
|
element_id.into(),
|
|
FONT_SIZE,
|
|
&[window.text_style().to_run(str_len)],
|
|
None,
|
|
None,
|
|
)
|
|
.ok()
|
|
.and_then(|mut text| text.pop())
|
|
{
|
|
text.paint(hitbox.origin, FONT_SIZE, TextAlign::Left, None, window, cx)
|
|
.ok();
|
|
|
|
let text_bounds = crate::Bounds {
|
|
origin: hitbox.origin,
|
|
size: text.size(FONT_SIZE),
|
|
};
|
|
if self.location.is_some()
|
|
&& text_bounds.contains(&window.mouse_position())
|
|
&& window.modifiers().secondary()
|
|
{
|
|
let secondary_held = window.modifiers().secondary();
|
|
window.on_key_event({
|
|
move |e: &crate::ModifiersChangedEvent, _phase, window, _cx| {
|
|
if e.modifiers.secondary() != secondary_held
|
|
&& text_bounds.contains(&window.mouse_position())
|
|
{
|
|
window.refresh();
|
|
}
|
|
}
|
|
});
|
|
|
|
let was_hovered = hitbox.is_hovered(window);
|
|
let current_view = window.current_view();
|
|
window.on_mouse_event({
|
|
let hitbox = hitbox.clone();
|
|
move |_: &MouseMoveEvent, phase, window, cx| {
|
|
if phase == DispatchPhase::Capture {
|
|
let hovered = hitbox.is_hovered(window);
|
|
if hovered != was_hovered {
|
|
cx.notify(current_view)
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
window.on_mouse_event({
|
|
let hitbox = hitbox.clone();
|
|
let location = self.location.unwrap();
|
|
move |e: &crate::MouseDownEvent, phase, window, cx| {
|
|
if text_bounds.contains(&e.position)
|
|
&& phase.capture()
|
|
&& hitbox.is_hovered(window)
|
|
{
|
|
cx.stop_propagation();
|
|
let Ok(dir) = std::env::current_dir() else {
|
|
return;
|
|
};
|
|
|
|
eprintln!(
|
|
"This element was created at:\n{}:{}:{}",
|
|
dir.join(location.file()).to_string_lossy(),
|
|
location.line(),
|
|
location.column()
|
|
);
|
|
}
|
|
}
|
|
});
|
|
window.paint_quad(crate::outline(
|
|
crate::Bounds {
|
|
origin: hitbox.origin
|
|
+ crate::point(crate::px(0.), FONT_SIZE - px(2.)),
|
|
size: crate::Size {
|
|
width: text_bounds.size.width,
|
|
height: crate::px(1.),
|
|
},
|
|
},
|
|
crate::red(),
|
|
BorderStyle::default(),
|
|
))
|
|
}
|
|
}
|
|
};
|
|
|
|
window.with_text_style(
|
|
Some(crate::TextStyleRefinement {
|
|
color: Some(crate::red()),
|
|
line_height: Some(FONT_SIZE.into()),
|
|
background_color: Some(crate::white()),
|
|
..Default::default()
|
|
}),
|
|
render_debug_text,
|
|
)
|
|
}
|
|
}
|
|
|
|
fn paint_mouse_listeners(
|
|
&mut self,
|
|
hitbox: &Hitbox,
|
|
element_state: Option<&mut InteractiveElementState>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
// If this element can be focused, register a mouse down listener
|
|
// that will automatically transfer focus when hitting the element.
|
|
// This behavior can be suppressed by using `cx.prevent_default()`.
|
|
if let Some(focus_handle) = self.tracked_focus_handle.clone() {
|
|
let hitbox = hitbox.clone();
|
|
window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _| {
|
|
if phase == DispatchPhase::Bubble
|
|
&& hitbox.is_hovered(window)
|
|
&& !window.default_prevented()
|
|
{
|
|
window.focus(&focus_handle);
|
|
// If there is a parent that is also focusable, prevent it
|
|
// from transferring focus because we already did so.
|
|
window.prevent_default();
|
|
}
|
|
});
|
|
}
|
|
|
|
for listener in self.mouse_down_listeners.drain(..) {
|
|
let hitbox = hitbox.clone();
|
|
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
|
listener(event, phase, &hitbox, window, cx);
|
|
})
|
|
}
|
|
|
|
for listener in self.mouse_up_listeners.drain(..) {
|
|
let hitbox = hitbox.clone();
|
|
window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| {
|
|
listener(event, phase, &hitbox, window, cx);
|
|
})
|
|
}
|
|
|
|
for listener in self.mouse_move_listeners.drain(..) {
|
|
let hitbox = hitbox.clone();
|
|
window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
|
|
listener(event, phase, &hitbox, window, cx);
|
|
})
|
|
}
|
|
|
|
for listener in self.scroll_wheel_listeners.drain(..) {
|
|
let hitbox = hitbox.clone();
|
|
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
|
|
listener(event, phase, &hitbox, window, cx);
|
|
})
|
|
}
|
|
|
|
if self.hover_style.is_some()
|
|
|| self.base_style.mouse_cursor.is_some()
|
|
|| cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
|
|
{
|
|
let hitbox = hitbox.clone();
|
|
let was_hovered = hitbox.is_hovered(window);
|
|
let current_view = window.current_view();
|
|
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| {
|
|
let hovered = hitbox.is_hovered(window);
|
|
if phase == DispatchPhase::Capture && hovered != was_hovered {
|
|
cx.notify(current_view);
|
|
}
|
|
});
|
|
}
|
|
let drag_cursor_style = self.base_style.as_ref().mouse_cursor;
|
|
|
|
let mut drag_listener = mem::take(&mut self.drag_listener);
|
|
let drop_listeners = mem::take(&mut self.drop_listeners);
|
|
let click_listeners = mem::take(&mut self.click_listeners);
|
|
let can_drop_predicate = mem::take(&mut self.can_drop_predicate);
|
|
|
|
if !drop_listeners.is_empty() {
|
|
let hitbox = hitbox.clone();
|
|
window.on_mouse_event({
|
|
move |_: &MouseUpEvent, phase, window, cx| {
|
|
if let Some(drag) = &cx.active_drag {
|
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
|
let drag_state_type = drag.value.as_ref().type_id();
|
|
for (drop_state_type, listener) in &drop_listeners {
|
|
if *drop_state_type == drag_state_type {
|
|
let drag = cx
|
|
.active_drag
|
|
.take()
|
|
.expect("checked for type drag state type above");
|
|
|
|
let mut can_drop = true;
|
|
if let Some(predicate) = &can_drop_predicate {
|
|
can_drop = predicate(drag.value.as_ref(), window, cx);
|
|
}
|
|
|
|
if can_drop {
|
|
listener(drag.value.as_ref(), window, cx);
|
|
window.refresh();
|
|
cx.stop_propagation();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if let Some(element_state) = element_state {
|
|
if !click_listeners.is_empty() || drag_listener.is_some() {
|
|
let pending_mouse_down = element_state
|
|
.pending_mouse_down
|
|
.get_or_insert_with(Default::default)
|
|
.clone();
|
|
|
|
let clicked_state = element_state
|
|
.clicked_state
|
|
.get_or_insert_with(Default::default)
|
|
.clone();
|
|
|
|
window.on_mouse_event({
|
|
let pending_mouse_down = pending_mouse_down.clone();
|
|
let hitbox = hitbox.clone();
|
|
move |event: &MouseDownEvent, phase, window, _cx| {
|
|
if phase == DispatchPhase::Bubble
|
|
&& event.button == MouseButton::Left
|
|
&& hitbox.is_hovered(window)
|
|
{
|
|
*pending_mouse_down.borrow_mut() = Some(event.clone());
|
|
window.refresh();
|
|
}
|
|
}
|
|
});
|
|
|
|
window.on_mouse_event({
|
|
let pending_mouse_down = pending_mouse_down.clone();
|
|
let hitbox = hitbox.clone();
|
|
move |event: &MouseMoveEvent, phase, window, cx| {
|
|
if phase == DispatchPhase::Capture {
|
|
return;
|
|
}
|
|
|
|
let mut pending_mouse_down = pending_mouse_down.borrow_mut();
|
|
if let Some(mouse_down) = pending_mouse_down.clone() {
|
|
if !cx.has_active_drag()
|
|
&& (event.position - mouse_down.position).magnitude()
|
|
> DRAG_THRESHOLD
|
|
{
|
|
if let Some((drag_value, drag_listener)) = drag_listener.take() {
|
|
*clicked_state.borrow_mut() = ElementClickedState::default();
|
|
let cursor_offset = event.position - hitbox.origin;
|
|
let drag = (drag_listener)(
|
|
drag_value.as_ref(),
|
|
cursor_offset,
|
|
window,
|
|
cx,
|
|
);
|
|
cx.active_drag = Some(AnyDrag {
|
|
view: drag,
|
|
value: drag_value,
|
|
cursor_offset,
|
|
cursor_style: drag_cursor_style,
|
|
});
|
|
pending_mouse_down.take();
|
|
window.refresh();
|
|
cx.stop_propagation();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
window.on_mouse_event({
|
|
let mut captured_mouse_down = None;
|
|
let hitbox = hitbox.clone();
|
|
move |event: &MouseUpEvent, phase, window, cx| match phase {
|
|
// Clear the pending mouse down during the capture phase,
|
|
// so that it happens even if another event handler stops
|
|
// propagation.
|
|
DispatchPhase::Capture => {
|
|
let mut pending_mouse_down = pending_mouse_down.borrow_mut();
|
|
if pending_mouse_down.is_some() && hitbox.is_hovered(window) {
|
|
captured_mouse_down = pending_mouse_down.take();
|
|
window.refresh();
|
|
} else if pending_mouse_down.is_some() {
|
|
// Clear the pending mouse down event (without firing click handlers)
|
|
// if the hitbox is not being hovered.
|
|
// This avoids dragging elements that changed their position
|
|
// immediately after being clicked.
|
|
// See https://github.com/zed-industries/zed/issues/24600 for more details
|
|
pending_mouse_down.take();
|
|
window.refresh();
|
|
}
|
|
}
|
|
// Fire click handlers during the bubble phase.
|
|
DispatchPhase::Bubble => {
|
|
if let Some(mouse_down) = captured_mouse_down.take() {
|
|
let mouse_click = ClickEvent {
|
|
down: mouse_down,
|
|
up: event.clone(),
|
|
};
|
|
for listener in &click_listeners {
|
|
listener(&mouse_click, window, cx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if let Some(hover_listener) = self.hover_listener.take() {
|
|
let hitbox = hitbox.clone();
|
|
let was_hovered = element_state
|
|
.hover_state
|
|
.get_or_insert_with(Default::default)
|
|
.clone();
|
|
let has_mouse_down = element_state
|
|
.pending_mouse_down
|
|
.get_or_insert_with(Default::default)
|
|
.clone();
|
|
|
|
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| {
|
|
if phase != DispatchPhase::Bubble {
|
|
return;
|
|
}
|
|
let is_hovered = has_mouse_down.borrow().is_none()
|
|
&& !cx.has_active_drag()
|
|
&& hitbox.is_hovered(window);
|
|
let mut was_hovered = was_hovered.borrow_mut();
|
|
|
|
if is_hovered != *was_hovered {
|
|
*was_hovered = is_hovered;
|
|
drop(was_hovered);
|
|
|
|
hover_listener(&is_hovered, window, cx);
|
|
}
|
|
});
|
|
}
|
|
|
|
if let Some(tooltip_builder) = self.tooltip_builder.take() {
|
|
let active_tooltip = element_state
|
|
.active_tooltip
|
|
.get_or_insert_with(Default::default)
|
|
.clone();
|
|
let pending_mouse_down = element_state
|
|
.pending_mouse_down
|
|
.get_or_insert_with(Default::default)
|
|
.clone();
|
|
|
|
let tooltip_is_hoverable = tooltip_builder.hoverable;
|
|
let build_tooltip = Rc::new(move |window: &mut Window, cx: &mut App| {
|
|
Some(((tooltip_builder.build)(window, cx), tooltip_is_hoverable))
|
|
});
|
|
// Use bounds instead of testing hitbox since this is called during prepaint.
|
|
let check_is_hovered_during_prepaint = Rc::new({
|
|
let pending_mouse_down = pending_mouse_down.clone();
|
|
let source_bounds = hitbox.bounds;
|
|
move |window: &Window| {
|
|
pending_mouse_down.borrow().is_none()
|
|
&& source_bounds.contains(&window.mouse_position())
|
|
}
|
|
});
|
|
let check_is_hovered = Rc::new({
|
|
let hitbox = hitbox.clone();
|
|
move |window: &Window| {
|
|
pending_mouse_down.borrow().is_none() && hitbox.is_hovered(window)
|
|
}
|
|
});
|
|
register_tooltip_mouse_handlers(
|
|
&active_tooltip,
|
|
self.tooltip_id,
|
|
build_tooltip,
|
|
check_is_hovered,
|
|
check_is_hovered_during_prepaint,
|
|
window,
|
|
);
|
|
}
|
|
|
|
let active_state = element_state
|
|
.clicked_state
|
|
.get_or_insert_with(Default::default)
|
|
.clone();
|
|
if active_state.borrow().is_clicked() {
|
|
window.on_mouse_event(move |_: &MouseUpEvent, phase, window, _cx| {
|
|
if phase == DispatchPhase::Capture {
|
|
*active_state.borrow_mut() = ElementClickedState::default();
|
|
window.refresh();
|
|
}
|
|
});
|
|
} else {
|
|
let active_group_hitbox = self
|
|
.group_active_style
|
|
.as_ref()
|
|
.and_then(|group_active| GroupHitboxes::get(&group_active.group, cx));
|
|
let hitbox = hitbox.clone();
|
|
window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _cx| {
|
|
if phase == DispatchPhase::Bubble && !window.default_prevented() {
|
|
let group_hovered = active_group_hitbox
|
|
.map_or(false, |group_hitbox_id| group_hitbox_id.is_hovered(window));
|
|
let element_hovered = hitbox.is_hovered(window);
|
|
if group_hovered || element_hovered {
|
|
*active_state.borrow_mut() = ElementClickedState {
|
|
group: group_hovered,
|
|
element: element_hovered,
|
|
};
|
|
window.refresh();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn paint_keyboard_listeners(&mut self, window: &mut Window, _cx: &mut App) {
|
|
let key_down_listeners = mem::take(&mut self.key_down_listeners);
|
|
let key_up_listeners = mem::take(&mut self.key_up_listeners);
|
|
let modifiers_changed_listeners = mem::take(&mut self.modifiers_changed_listeners);
|
|
let action_listeners = mem::take(&mut self.action_listeners);
|
|
if let Some(context) = self.key_context.clone() {
|
|
window.set_key_context(context);
|
|
}
|
|
|
|
for listener in key_down_listeners {
|
|
window.on_key_event(move |event: &KeyDownEvent, phase, window, cx| {
|
|
listener(event, phase, window, cx);
|
|
})
|
|
}
|
|
|
|
for listener in key_up_listeners {
|
|
window.on_key_event(move |event: &KeyUpEvent, phase, window, cx| {
|
|
listener(event, phase, window, cx);
|
|
})
|
|
}
|
|
|
|
for listener in modifiers_changed_listeners {
|
|
window.on_modifiers_changed(move |event: &ModifiersChangedEvent, window, cx| {
|
|
listener(event, window, cx);
|
|
})
|
|
}
|
|
|
|
for (action_type, listener) in action_listeners {
|
|
window.on_action(action_type, listener)
|
|
}
|
|
}
|
|
|
|
fn paint_hover_group_handler(&self, window: &mut Window, cx: &mut App) {
|
|
let group_hitbox = self
|
|
.group_hover_style
|
|
.as_ref()
|
|
.and_then(|group_hover| GroupHitboxes::get(&group_hover.group, cx));
|
|
|
|
if let Some(group_hitbox) = group_hitbox {
|
|
let was_hovered = group_hitbox.is_hovered(window);
|
|
let current_view = window.current_view();
|
|
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| {
|
|
let hovered = group_hitbox.is_hovered(window);
|
|
if phase == DispatchPhase::Capture && hovered != was_hovered {
|
|
cx.notify(current_view);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fn paint_scroll_listener(
|
|
&self,
|
|
hitbox: &Hitbox,
|
|
style: &Style,
|
|
window: &mut Window,
|
|
_cx: &mut App,
|
|
) {
|
|
if let Some(scroll_offset) = self.scroll_offset.clone() {
|
|
let overflow = style.overflow;
|
|
let allow_concurrent_scroll = style.allow_concurrent_scroll;
|
|
let restrict_scroll_to_axis = style.restrict_scroll_to_axis;
|
|
let line_height = window.line_height();
|
|
let hitbox = hitbox.clone();
|
|
let current_view = window.current_view();
|
|
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
|
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
|
let mut scroll_offset = scroll_offset.borrow_mut();
|
|
let old_scroll_offset = *scroll_offset;
|
|
let delta = event.delta.pixel_delta(line_height);
|
|
|
|
let mut delta_x = Pixels::ZERO;
|
|
if overflow.x == Overflow::Scroll {
|
|
if !delta.x.is_zero() {
|
|
delta_x = delta.x;
|
|
} else if !restrict_scroll_to_axis && overflow.y != Overflow::Scroll {
|
|
delta_x = delta.y;
|
|
}
|
|
}
|
|
let mut delta_y = Pixels::ZERO;
|
|
if overflow.y == Overflow::Scroll {
|
|
if !delta.y.is_zero() {
|
|
delta_y = delta.y;
|
|
} else if !restrict_scroll_to_axis && overflow.x != Overflow::Scroll {
|
|
delta_y = delta.x;
|
|
}
|
|
}
|
|
if !allow_concurrent_scroll && !delta_x.is_zero() && !delta_y.is_zero() {
|
|
if delta_x.abs() > delta_y.abs() {
|
|
delta_y = Pixels::ZERO;
|
|
} else {
|
|
delta_x = Pixels::ZERO;
|
|
}
|
|
}
|
|
scroll_offset.y += delta_y;
|
|
scroll_offset.x += delta_x;
|
|
cx.stop_propagation();
|
|
if *scroll_offset != old_scroll_offset {
|
|
cx.notify(current_view);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Compute the visual style for this element, based on the current bounds and the element's state.
|
|
pub fn compute_style(
|
|
&self,
|
|
global_id: Option<&GlobalElementId>,
|
|
hitbox: Option<&Hitbox>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Style {
|
|
window.with_optional_element_state(global_id, |element_state, window| {
|
|
let mut element_state =
|
|
element_state.map(|element_state| element_state.unwrap_or_default());
|
|
let style = self.compute_style_internal(hitbox, element_state.as_mut(), window, cx);
|
|
(style, element_state)
|
|
})
|
|
}
|
|
|
|
/// Called from internal methods that have already called with_element_state.
|
|
fn compute_style_internal(
|
|
&self,
|
|
hitbox: Option<&Hitbox>,
|
|
element_state: Option<&mut InteractiveElementState>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Style {
|
|
let mut style = Style::default();
|
|
style.refine(&self.base_style);
|
|
|
|
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
|
if let Some(in_focus_style) = self.in_focus_style.as_ref() {
|
|
if focus_handle.within_focused(window, cx) {
|
|
style.refine(in_focus_style);
|
|
}
|
|
}
|
|
|
|
if let Some(focus_style) = self.focus_style.as_ref() {
|
|
if focus_handle.is_focused(window) {
|
|
style.refine(focus_style);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(hitbox) = hitbox {
|
|
if !cx.has_active_drag() {
|
|
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
|
if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) {
|
|
if group_hitbox_id.is_hovered(window) {
|
|
style.refine(&group_hover.style);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(hover_style) = self.hover_style.as_ref() {
|
|
if hitbox.is_hovered(window) {
|
|
style.refine(hover_style);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(drag) = cx.active_drag.take() {
|
|
let mut can_drop = true;
|
|
if let Some(can_drop_predicate) = &self.can_drop_predicate {
|
|
can_drop = can_drop_predicate(drag.value.as_ref(), window, cx);
|
|
}
|
|
|
|
if can_drop {
|
|
for (state_type, group_drag_style) in &self.group_drag_over_styles {
|
|
if let Some(group_hitbox_id) =
|
|
GroupHitboxes::get(&group_drag_style.group, cx)
|
|
{
|
|
if *state_type == drag.value.as_ref().type_id()
|
|
&& group_hitbox_id.is_hovered(window)
|
|
{
|
|
style.refine(&group_drag_style.style);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (state_type, build_drag_over_style) in &self.drag_over_styles {
|
|
if *state_type == drag.value.as_ref().type_id() && hitbox.is_hovered(window)
|
|
{
|
|
style.refine(&build_drag_over_style(drag.value.as_ref(), window, cx));
|
|
}
|
|
}
|
|
}
|
|
|
|
style.mouse_cursor = drag.cursor_style;
|
|
cx.active_drag = Some(drag);
|
|
}
|
|
}
|
|
|
|
if let Some(element_state) = element_state {
|
|
let clicked_state = element_state
|
|
.clicked_state
|
|
.get_or_insert_with(Default::default)
|
|
.borrow();
|
|
if clicked_state.group {
|
|
if let Some(group) = self.group_active_style.as_ref() {
|
|
style.refine(&group.style)
|
|
}
|
|
}
|
|
|
|
if let Some(active_style) = self.active_style.as_ref() {
|
|
if clicked_state.element {
|
|
style.refine(active_style)
|
|
}
|
|
}
|
|
}
|
|
|
|
style
|
|
}
|
|
}
|
|
|
|
/// The per-frame state of an interactive element. Used for tracking stateful interactions like clicks
|
|
/// and scroll offsets.
|
|
#[derive(Default)]
|
|
pub struct InteractiveElementState {
|
|
pub(crate) focus_handle: Option<FocusHandle>,
|
|
pub(crate) clicked_state: Option<Rc<RefCell<ElementClickedState>>>,
|
|
pub(crate) hover_state: Option<Rc<RefCell<bool>>>,
|
|
pub(crate) pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
|
|
pub(crate) scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
|
|
pub(crate) active_tooltip: Option<Rc<RefCell<Option<ActiveTooltip>>>>,
|
|
}
|
|
|
|
/// Whether or not the element or a group that contains it is clicked by the mouse.
|
|
#[derive(Copy, Clone, Default, Eq, PartialEq)]
|
|
pub struct ElementClickedState {
|
|
/// True if this element's group has been clicked, false otherwise
|
|
pub group: bool,
|
|
|
|
/// True if this element has been clicked, false otherwise
|
|
pub element: bool,
|
|
}
|
|
|
|
impl ElementClickedState {
|
|
fn is_clicked(&self) -> bool {
|
|
self.group || self.element
|
|
}
|
|
}
|
|
|
|
pub(crate) enum ActiveTooltip {
|
|
/// Currently delaying before showing the tooltip.
|
|
WaitingForShow { _task: Task<()> },
|
|
/// Tooltip is visible, element was hovered or for hoverable tooltips, the tooltip was hovered.
|
|
Visible {
|
|
tooltip: AnyTooltip,
|
|
is_hoverable: bool,
|
|
},
|
|
/// Tooltip is visible and hoverable, but the mouse is no longer hovering. Currently delaying
|
|
/// before hiding it.
|
|
WaitingForHide {
|
|
tooltip: AnyTooltip,
|
|
_task: Task<()>,
|
|
},
|
|
}
|
|
|
|
pub(crate) fn clear_active_tooltip(
|
|
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
|
window: &mut Window,
|
|
) {
|
|
match active_tooltip.borrow_mut().take() {
|
|
None => {}
|
|
Some(ActiveTooltip::WaitingForShow { .. }) => {}
|
|
Some(ActiveTooltip::Visible { .. }) => window.refresh(),
|
|
Some(ActiveTooltip::WaitingForHide { .. }) => window.refresh(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn clear_active_tooltip_if_not_hoverable(
|
|
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
|
window: &mut Window,
|
|
) {
|
|
let should_clear = match active_tooltip.borrow().as_ref() {
|
|
None => false,
|
|
Some(ActiveTooltip::WaitingForShow { .. }) => false,
|
|
Some(ActiveTooltip::Visible { is_hoverable, .. }) => !is_hoverable,
|
|
Some(ActiveTooltip::WaitingForHide { .. }) => false,
|
|
};
|
|
if should_clear {
|
|
active_tooltip.borrow_mut().take();
|
|
window.refresh();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn set_tooltip_on_window(
|
|
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
|
window: &mut Window,
|
|
) -> Option<TooltipId> {
|
|
let tooltip = match active_tooltip.borrow().as_ref() {
|
|
None => return None,
|
|
Some(ActiveTooltip::WaitingForShow { .. }) => return None,
|
|
Some(ActiveTooltip::Visible { tooltip, .. }) => tooltip.clone(),
|
|
Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => tooltip.clone(),
|
|
};
|
|
Some(window.set_tooltip(tooltip))
|
|
}
|
|
|
|
pub(crate) fn register_tooltip_mouse_handlers(
|
|
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
|
tooltip_id: Option<TooltipId>,
|
|
build_tooltip: Rc<dyn Fn(&mut Window, &mut App) -> Option<(AnyView, bool)>>,
|
|
check_is_hovered: Rc<dyn Fn(&Window) -> bool>,
|
|
check_is_hovered_during_prepaint: Rc<dyn Fn(&Window) -> bool>,
|
|
window: &mut Window,
|
|
) {
|
|
window.on_mouse_event({
|
|
let active_tooltip = active_tooltip.clone();
|
|
let build_tooltip = build_tooltip.clone();
|
|
let check_is_hovered = check_is_hovered.clone();
|
|
move |_: &MouseMoveEvent, phase, window, cx| {
|
|
handle_tooltip_mouse_move(
|
|
&active_tooltip,
|
|
&build_tooltip,
|
|
&check_is_hovered,
|
|
&check_is_hovered_during_prepaint,
|
|
phase,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
});
|
|
|
|
window.on_mouse_event({
|
|
let active_tooltip = active_tooltip.clone();
|
|
move |_: &MouseDownEvent, _phase, window: &mut Window, _cx| {
|
|
if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) {
|
|
clear_active_tooltip_if_not_hoverable(&active_tooltip, window);
|
|
}
|
|
}
|
|
});
|
|
|
|
window.on_mouse_event({
|
|
let active_tooltip = active_tooltip.clone();
|
|
move |_: &ScrollWheelEvent, _phase, window: &mut Window, _cx| {
|
|
if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) {
|
|
clear_active_tooltip_if_not_hoverable(&active_tooltip, window);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Handles displaying tooltips when an element is hovered.
|
|
///
|
|
/// The mouse hovering logic also relies on being called from window prepaint in order to handle the
|
|
/// case where the element the tooltip is on is not rendered - in that case its mouse listeners are
|
|
/// also not registered. During window prepaint, the hitbox information is not available, so
|
|
/// `check_is_hovered_during_prepaint` is used which bases the check off of the absolute bounds of
|
|
/// the element.
|
|
///
|
|
/// TODO: There's a minor bug due to the use of absolute bounds while checking during prepaint - it
|
|
/// does not know if the hitbox is occluded. In the case where a tooltip gets displayed and then
|
|
/// gets occluded after display, it will stick around until the mouse exits the hover bounds.
|
|
fn handle_tooltip_mouse_move(
|
|
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
|
build_tooltip: &Rc<dyn Fn(&mut Window, &mut App) -> Option<(AnyView, bool)>>,
|
|
check_is_hovered: &Rc<dyn Fn(&Window) -> bool>,
|
|
check_is_hovered_during_prepaint: &Rc<dyn Fn(&Window) -> bool>,
|
|
phase: DispatchPhase,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
// Separates logic for what mutation should occur from applying it, to avoid overlapping
|
|
// RefCell borrows.
|
|
enum Action {
|
|
None,
|
|
CancelShow,
|
|
ScheduleShow,
|
|
}
|
|
|
|
let action = match active_tooltip.borrow().as_ref() {
|
|
None => {
|
|
let is_hovered = check_is_hovered(window);
|
|
if is_hovered && phase.bubble() {
|
|
Action::ScheduleShow
|
|
} else {
|
|
Action::None
|
|
}
|
|
}
|
|
Some(ActiveTooltip::WaitingForShow { .. }) => {
|
|
let is_hovered = check_is_hovered(window);
|
|
if is_hovered {
|
|
Action::None
|
|
} else {
|
|
Action::CancelShow
|
|
}
|
|
}
|
|
// These are handled in check_visible_and_update.
|
|
Some(ActiveTooltip::Visible { .. }) | Some(ActiveTooltip::WaitingForHide { .. }) => {
|
|
Action::None
|
|
}
|
|
};
|
|
|
|
match action {
|
|
Action::None => {}
|
|
Action::CancelShow => {
|
|
// Cancel waiting to show tooltip when it is no longer hovered.
|
|
active_tooltip.borrow_mut().take();
|
|
}
|
|
Action::ScheduleShow => {
|
|
let delayed_show_task = window.spawn(cx, {
|
|
let active_tooltip = active_tooltip.clone();
|
|
let build_tooltip = build_tooltip.clone();
|
|
let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone();
|
|
async move |cx| {
|
|
cx.background_executor().timer(TOOLTIP_SHOW_DELAY).await;
|
|
cx.update(|window, cx| {
|
|
let new_tooltip =
|
|
build_tooltip(window, cx).map(|(view, tooltip_is_hoverable)| {
|
|
let active_tooltip = active_tooltip.clone();
|
|
ActiveTooltip::Visible {
|
|
tooltip: AnyTooltip {
|
|
view,
|
|
mouse_position: window.mouse_position(),
|
|
check_visible_and_update: Rc::new(
|
|
move |tooltip_bounds, window, cx| {
|
|
handle_tooltip_check_visible_and_update(
|
|
&active_tooltip,
|
|
tooltip_is_hoverable,
|
|
&check_is_hovered_during_prepaint,
|
|
tooltip_bounds,
|
|
window,
|
|
cx,
|
|
)
|
|
},
|
|
),
|
|
},
|
|
is_hoverable: tooltip_is_hoverable,
|
|
}
|
|
});
|
|
*active_tooltip.borrow_mut() = new_tooltip;
|
|
window.refresh();
|
|
})
|
|
.ok();
|
|
}
|
|
});
|
|
active_tooltip
|
|
.borrow_mut()
|
|
.replace(ActiveTooltip::WaitingForShow {
|
|
_task: delayed_show_task,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a callback which will be called by window prepaint to update tooltip visibility. The
|
|
/// purpose of doing this logic here instead of the mouse move handler is that the mouse move
|
|
/// handler won't get called when the element is not painted (e.g. via use of `visible_on_hover`).
|
|
fn handle_tooltip_check_visible_and_update(
|
|
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
|
tooltip_is_hoverable: bool,
|
|
check_is_hovered: &Rc<dyn Fn(&Window) -> bool>,
|
|
tooltip_bounds: Bounds<Pixels>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> bool {
|
|
// Separates logic for what mutation should occur from applying it, to avoid overlapping RefCell
|
|
// borrows.
|
|
enum Action {
|
|
None,
|
|
Hide,
|
|
ScheduleHide(AnyTooltip),
|
|
CancelHide(AnyTooltip),
|
|
}
|
|
|
|
let is_hovered = check_is_hovered(window)
|
|
|| (tooltip_is_hoverable && tooltip_bounds.contains(&window.mouse_position()));
|
|
let action = match active_tooltip.borrow().as_ref() {
|
|
Some(ActiveTooltip::Visible { tooltip, .. }) => {
|
|
if is_hovered {
|
|
Action::None
|
|
} else {
|
|
if tooltip_is_hoverable {
|
|
Action::ScheduleHide(tooltip.clone())
|
|
} else {
|
|
Action::Hide
|
|
}
|
|
}
|
|
}
|
|
Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => {
|
|
if is_hovered {
|
|
Action::CancelHide(tooltip.clone())
|
|
} else {
|
|
Action::None
|
|
}
|
|
}
|
|
None | Some(ActiveTooltip::WaitingForShow { .. }) => Action::None,
|
|
};
|
|
|
|
match action {
|
|
Action::None => {}
|
|
Action::Hide => clear_active_tooltip(&active_tooltip, window),
|
|
Action::ScheduleHide(tooltip) => {
|
|
let delayed_hide_task = window.spawn(cx, {
|
|
let active_tooltip = active_tooltip.clone();
|
|
async move |cx| {
|
|
cx.background_executor()
|
|
.timer(HOVERABLE_TOOLTIP_HIDE_DELAY)
|
|
.await;
|
|
if active_tooltip.borrow_mut().take().is_some() {
|
|
cx.update(|window, _cx| window.refresh()).ok();
|
|
}
|
|
}
|
|
});
|
|
active_tooltip
|
|
.borrow_mut()
|
|
.replace(ActiveTooltip::WaitingForHide {
|
|
tooltip,
|
|
_task: delayed_hide_task,
|
|
});
|
|
}
|
|
Action::CancelHide(tooltip) => {
|
|
// Cancel waiting to hide tooltip when it becomes hovered.
|
|
active_tooltip.borrow_mut().replace(ActiveTooltip::Visible {
|
|
tooltip,
|
|
is_hoverable: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
active_tooltip.borrow().is_some()
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct GroupHitboxes(HashMap<SharedString, SmallVec<[HitboxId; 1]>>);
|
|
|
|
impl Global for GroupHitboxes {}
|
|
|
|
impl GroupHitboxes {
|
|
pub fn get(name: &SharedString, cx: &mut App) -> Option<HitboxId> {
|
|
cx.default_global::<Self>()
|
|
.0
|
|
.get(name)
|
|
.and_then(|bounds_stack| bounds_stack.last())
|
|
.cloned()
|
|
}
|
|
|
|
pub fn push(name: SharedString, hitbox_id: HitboxId, cx: &mut App) {
|
|
cx.default_global::<Self>()
|
|
.0
|
|
.entry(name)
|
|
.or_default()
|
|
.push(hitbox_id);
|
|
}
|
|
|
|
pub fn pop(name: &SharedString, cx: &mut App) {
|
|
cx.default_global::<Self>().0.get_mut(name).unwrap().pop();
|
|
}
|
|
}
|
|
|
|
/// A wrapper around an element that can be focused.
|
|
pub struct FocusableWrapper<E> {
|
|
/// The element that is focusable
|
|
pub element: E,
|
|
}
|
|
|
|
impl<E: InteractiveElement> FocusableElement for FocusableWrapper<E> {}
|
|
|
|
impl<E> InteractiveElement for FocusableWrapper<E>
|
|
where
|
|
E: InteractiveElement,
|
|
{
|
|
fn interactivity(&mut self) -> &mut Interactivity {
|
|
self.element.interactivity()
|
|
}
|
|
}
|
|
|
|
impl<E: StatefulInteractiveElement> StatefulInteractiveElement for FocusableWrapper<E> {}
|
|
|
|
impl<E> Styled for FocusableWrapper<E>
|
|
where
|
|
E: Styled,
|
|
{
|
|
fn style(&mut self) -> &mut StyleRefinement {
|
|
self.element.style()
|
|
}
|
|
}
|
|
|
|
impl FocusableWrapper<Div> {
|
|
/// Add a listener to be called when the children of this `Div` are prepainted.
|
|
/// This allows you to store the [`Bounds`] of the children for later use.
|
|
pub fn on_children_prepainted(
|
|
mut self,
|
|
listener: impl Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.element = self.element.on_children_prepainted(listener);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl<E> Element for FocusableWrapper<E>
|
|
where
|
|
E: Element,
|
|
{
|
|
type RequestLayoutState = E::RequestLayoutState;
|
|
type PrepaintState = E::PrepaintState;
|
|
|
|
fn id(&self) -> Option<ElementId> {
|
|
self.element.id()
|
|
}
|
|
|
|
fn request_layout(
|
|
&mut self,
|
|
id: Option<&GlobalElementId>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> (LayoutId, Self::RequestLayoutState) {
|
|
self.element.request_layout(id, window, cx)
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
id: Option<&GlobalElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
state: &mut Self::RequestLayoutState,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> E::PrepaintState {
|
|
self.element.prepaint(id, bounds, state, window, cx)
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
id: Option<&GlobalElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
request_layout: &mut Self::RequestLayoutState,
|
|
prepaint: &mut Self::PrepaintState,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
self.element
|
|
.paint(id, bounds, request_layout, prepaint, window, cx)
|
|
}
|
|
}
|
|
|
|
impl<E> IntoElement for FocusableWrapper<E>
|
|
where
|
|
E: IntoElement,
|
|
{
|
|
type Element = E::Element;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self.element.into_element()
|
|
}
|
|
}
|
|
|
|
impl<E> ParentElement for FocusableWrapper<E>
|
|
where
|
|
E: ParentElement,
|
|
{
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.element.extend(elements)
|
|
}
|
|
}
|
|
|
|
/// A wrapper around an element that can store state, produced after assigning an ElementId.
|
|
pub struct Stateful<E> {
|
|
pub(crate) element: E,
|
|
}
|
|
|
|
impl<E> Styled for Stateful<E>
|
|
where
|
|
E: Styled,
|
|
{
|
|
fn style(&mut self) -> &mut StyleRefinement {
|
|
self.element.style()
|
|
}
|
|
}
|
|
|
|
impl<E> StatefulInteractiveElement for Stateful<E>
|
|
where
|
|
E: Element,
|
|
Self: InteractiveElement,
|
|
{
|
|
}
|
|
|
|
impl<E> InteractiveElement for Stateful<E>
|
|
where
|
|
E: InteractiveElement,
|
|
{
|
|
fn interactivity(&mut self) -> &mut Interactivity {
|
|
self.element.interactivity()
|
|
}
|
|
}
|
|
|
|
impl<E: FocusableElement> FocusableElement for Stateful<E> {}
|
|
|
|
impl<E> Element for Stateful<E>
|
|
where
|
|
E: Element,
|
|
{
|
|
type RequestLayoutState = E::RequestLayoutState;
|
|
type PrepaintState = E::PrepaintState;
|
|
|
|
fn id(&self) -> Option<ElementId> {
|
|
self.element.id()
|
|
}
|
|
|
|
fn request_layout(
|
|
&mut self,
|
|
id: Option<&GlobalElementId>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> (LayoutId, Self::RequestLayoutState) {
|
|
self.element.request_layout(id, window, cx)
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
id: Option<&GlobalElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
state: &mut Self::RequestLayoutState,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> E::PrepaintState {
|
|
self.element.prepaint(id, bounds, state, window, cx)
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
id: Option<&GlobalElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
request_layout: &mut Self::RequestLayoutState,
|
|
prepaint: &mut Self::PrepaintState,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
self.element
|
|
.paint(id, bounds, request_layout, prepaint, window, cx);
|
|
}
|
|
}
|
|
|
|
impl<E> IntoElement for Stateful<E>
|
|
where
|
|
E: Element,
|
|
{
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl<E> ParentElement for Stateful<E>
|
|
where
|
|
E: ParentElement,
|
|
{
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.element.extend(elements)
|
|
}
|
|
}
|
|
|
|
/// Represents an element that can be scrolled *to* in its parent element.
|
|
///
|
|
/// Contrary to [ScrollHandle::scroll_to_item], an anchored element does not have to be an immediate child of the parent.
|
|
#[derive(Clone)]
|
|
pub struct ScrollAnchor {
|
|
handle: ScrollHandle,
|
|
last_origin: Rc<RefCell<Point<Pixels>>>,
|
|
}
|
|
|
|
impl ScrollAnchor {
|
|
/// Creates a [ScrollAnchor] associated with a given [ScrollHandle].
|
|
pub fn for_handle(handle: ScrollHandle) -> Self {
|
|
Self {
|
|
handle,
|
|
last_origin: Default::default(),
|
|
}
|
|
}
|
|
/// Request scroll to this item on the next frame.
|
|
pub fn scroll_to(&self, window: &mut Window, _cx: &mut App) {
|
|
let this = self.clone();
|
|
|
|
window.on_next_frame(move |_, _| {
|
|
let viewport_bounds = this.handle.bounds();
|
|
let self_bounds = *this.last_origin.borrow();
|
|
this.handle.set_offset(viewport_bounds.origin - self_bounds);
|
|
});
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Debug)]
|
|
struct ScrollHandleState {
|
|
offset: Rc<RefCell<Point<Pixels>>>,
|
|
bounds: Bounds<Pixels>,
|
|
padded_content_size: Size<Pixels>,
|
|
child_bounds: Vec<Bounds<Pixels>>,
|
|
scroll_to_bottom: bool,
|
|
overflow: Point<Overflow>,
|
|
}
|
|
|
|
/// A handle to the scrollable aspects of an element.
|
|
/// Used for accessing scroll state, like the current scroll offset,
|
|
/// and for mutating the scroll state, like scrolling to a specific child.
|
|
#[derive(Clone, Debug)]
|
|
pub struct ScrollHandle(Rc<RefCell<ScrollHandleState>>);
|
|
|
|
impl Default for ScrollHandle {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl ScrollHandle {
|
|
/// Construct a new scroll handle.
|
|
pub fn new() -> Self {
|
|
Self(Rc::default())
|
|
}
|
|
|
|
/// Get the current scroll offset.
|
|
pub fn offset(&self) -> Point<Pixels> {
|
|
*self.0.borrow().offset.borrow()
|
|
}
|
|
|
|
/// Get the top child that's scrolled into view.
|
|
pub fn top_item(&self) -> usize {
|
|
let state = self.0.borrow();
|
|
let top = state.bounds.top() - state.offset.borrow().y;
|
|
|
|
match state.child_bounds.binary_search_by(|bounds| {
|
|
if top < bounds.top() {
|
|
Ordering::Greater
|
|
} else if top > bounds.bottom() {
|
|
Ordering::Less
|
|
} else {
|
|
Ordering::Equal
|
|
}
|
|
}) {
|
|
Ok(ix) => ix,
|
|
Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)),
|
|
}
|
|
}
|
|
|
|
/// Return the bounds into which this child is painted
|
|
pub fn bounds(&self) -> Bounds<Pixels> {
|
|
self.0.borrow().bounds
|
|
}
|
|
|
|
/// Set the bounds into which this child is painted
|
|
pub(super) fn set_bounds(&self, bounds: Bounds<Pixels>) {
|
|
self.0.borrow_mut().bounds = bounds;
|
|
}
|
|
|
|
/// Get the bounds for a specific child.
|
|
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
|
|
self.0.borrow().child_bounds.get(ix).cloned()
|
|
}
|
|
|
|
/// Get the size of the content with padding of the container.
|
|
pub fn padded_content_size(&self) -> Size<Pixels> {
|
|
self.0.borrow().padded_content_size
|
|
}
|
|
|
|
/// scroll_to_item scrolls the minimal amount to ensure that the child is
|
|
/// fully visible
|
|
pub fn scroll_to_item(&self, ix: usize) {
|
|
let state = self.0.borrow();
|
|
|
|
let Some(bounds) = state.child_bounds.get(ix) else {
|
|
return;
|
|
};
|
|
|
|
let mut scroll_offset = state.offset.borrow_mut();
|
|
|
|
if state.overflow.y == Overflow::Scroll {
|
|
if bounds.top() + scroll_offset.y < state.bounds.top() {
|
|
scroll_offset.y = state.bounds.top() - bounds.top();
|
|
} else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() {
|
|
scroll_offset.y = state.bounds.bottom() - bounds.bottom();
|
|
}
|
|
}
|
|
|
|
if state.overflow.x == Overflow::Scroll {
|
|
if bounds.left() + scroll_offset.x < state.bounds.left() {
|
|
scroll_offset.x = state.bounds.left() - bounds.left();
|
|
} else if bounds.right() + scroll_offset.x > state.bounds.right() {
|
|
scroll_offset.x = state.bounds.right() - bounds.right();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Scrolls to the bottom.
|
|
pub fn scroll_to_bottom(&self) {
|
|
let mut state = self.0.borrow_mut();
|
|
state.scroll_to_bottom = true;
|
|
}
|
|
|
|
/// Set the offset explicitly. The offset is the distance from the top left of the
|
|
/// parent container to the top left of the first child.
|
|
/// As you scroll further down the offset becomes more negative.
|
|
pub fn set_offset(&self, mut position: Point<Pixels>) {
|
|
let state = self.0.borrow();
|
|
*state.offset.borrow_mut() = position;
|
|
}
|
|
|
|
/// Get the logical scroll top, based on a child index and a pixel offset.
|
|
pub fn logical_scroll_top(&self) -> (usize, Pixels) {
|
|
let ix = self.top_item();
|
|
let state = self.0.borrow();
|
|
|
|
if let Some(child_bounds) = state.child_bounds.get(ix) {
|
|
(
|
|
ix,
|
|
child_bounds.top() + state.offset.borrow().y - state.bounds.top(),
|
|
)
|
|
} else {
|
|
(ix, px(0.))
|
|
}
|
|
}
|
|
|
|
/// Get the count of children for scrollable item.
|
|
pub fn children_count(&self) -> usize {
|
|
self.0.borrow().child_bounds.len()
|
|
}
|
|
}
|