use std::rc::Rc;
use gpui_kit::{
AnyElement, App, Bounds, ClickEvent, ElementId, Entity, FontWeight, Hitbox, HitboxBehavior,
InteractiveElement as _, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, ParentElement as _, Pixels, Point, RenderOnce, SharedString, Styled as _,
TestSupportExt as _, TouchDragEvent, TouchPhase, Window,
base::{
POPUP_PRIORITY, Placement, Positioner, SelectionEdge, TouchHandle, TouchSelectionSnapshot,
input::{self, InputBaseState, InputModeKind},
},
canvas, deferred, div, px,
};
use crate::{ActiveTheme as _, Button};
type SnapshotSource = Rc<dyn Fn(&Window, &App) -> Option<TouchSelectionSnapshot>>;
type DragHandler = Rc<dyn Fn(SelectionEdge, TouchPhase, Point<Pixels>, &mut Window, &mut App)>;
type ItemHandler = Rc<dyn Fn(&mut Window, &mut App)>;
pub(crate) struct EditMenuItem {
label: SharedString,
on_click: ItemHandler,
}
impl EditMenuItem {
pub(crate) fn new(
label: impl Into<SharedString>,
on_click: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self {
label: label.into(),
on_click: Rc::new(on_click),
}
}
}
pub(crate) struct TouchSelectionOverlay {
id: ElementId,
source: SnapshotSource,
items: Vec<EditMenuItem>,
on_drag: Option<DragHandler>,
}
impl TouchSelectionOverlay {
pub(crate) fn new(
id: impl Into<ElementId>,
source: impl Fn(&Window, &App) -> Option<TouchSelectionSnapshot> + 'static,
) -> Self {
Self {
id: id.into(),
source: Rc::new(source),
items: Vec::new(),
on_drag: None,
}
}
pub(crate) fn handles(
mut self,
on_drag: impl Fn(SelectionEdge, TouchPhase, Point<Pixels>, &mut Window, &mut App) + 'static,
) -> Self {
self.on_drag = Some(Rc::new(on_drag));
self
}
pub(crate) fn items(mut self, items: impl IntoIterator<Item = EditMenuItem>) -> Self {
self.items.extend(items);
self
}
pub(crate) fn into_elements(self, window: &Window, cx: &App) -> Vec<AnyElement> {
let Some(snapshot) = (self.source)(window, cx) else {
return Vec::new();
};
let mut elements = Vec::with_capacity(2);
if let Some(on_drag) = self.on_drag {
elements.push(SelectionHandles::new(self.source, on_drag).into_any_element());
}
if let Some(mut anchor) = snapshot
.bounds()
.filter(|_| snapshot.is_menu_open() && !self.items.is_empty())
{
if !snapshot.is_empty() {
anchor.origin.y -= TouchHandle::EXTENT;
anchor.size.height += TouchHandle::EXTENT * 2.;
}
elements.push(
EditMenu::new(self.id, anchor)
.items(self.items)
.into_any_element(),
);
}
elements
}
}
#[derive(IntoElement)]
struct SelectionHandles {
source: SnapshotSource,
on_drag: DragHandler,
}
struct LaidOutHandle {
edge: SelectionEdge,
caret: Bounds<Pixels>,
hitbox: Hitbox,
}
impl SelectionHandles {
fn new(source: SnapshotSource, on_drag: DragHandler) -> Self {
Self { source, on_drag }
}
fn layout(source: &SnapshotSource, window: &mut Window, cx: &mut App) -> Vec<LaidOutHandle> {
let Some(snapshot) = source(window, cx) else {
return Vec::new();
};
let window_bounds = Bounds::new(Point::default(), window.viewport_size());
let mut handles = Vec::with_capacity(2);
if !snapshot.is_empty() {
for edge in [SelectionEdge::Start, SelectionEdge::End] {
let caret = snapshot.edge(edge);
if !snapshot.is_edge_visible(edge) || !window_bounds.contains(&caret.origin) {
continue;
}
let hitbox = window.insert_hitbox(
TouchHandle::hit_bounds(edge, caret),
HitboxBehavior::BlockMouse,
);
handles.push(LaidOutHandle {
edge,
caret,
hitbox,
});
}
}
handles
}
fn listen(
handles: &[LaidOutHandle],
source: &SnapshotSource,
on_drag: &DragHandler,
window: &mut Window,
) {
for handle in handles {
window.on_mouse_event({
let hitbox = handle.hitbox.clone();
let edge = handle.edge;
let on_drag = on_drag.clone();
move |event: &TouchDragEvent, phase, window, cx| {
if !phase.bubble()
|| event.phase != TouchPhase::Started
|| window.default_prevented()
|| !hitbox.is_hovered(window)
{
return;
}
window.prevent_default();
cx.stop_propagation();
on_drag(edge, TouchPhase::Started, event.position, window, cx);
}
});
window.on_mouse_event({
let hitbox = handle.hitbox.clone();
let edge = handle.edge;
let on_drag = on_drag.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if !phase.bubble()
|| event.button != MouseButton::Left
|| !hitbox.is_hovered(window)
{
return;
}
cx.stop_propagation();
on_drag(edge, TouchPhase::Started, event.position, window, cx);
}
});
}
let dragging = {
let source = source.clone();
move |window: &Window, cx: &App| source(window, cx).and_then(|s| s.dragging())
};
window.on_mouse_event({
let on_drag = on_drag.clone();
let dragging = dragging.clone();
move |event: &TouchDragEvent, phase, window, cx| {
if !phase.bubble() || event.phase == TouchPhase::Started {
return;
}
let Some(edge) = dragging(window, cx) else {
return;
};
cx.stop_propagation();
on_drag(edge, event.phase, event.position, window, cx);
}
});
window.on_mouse_event({
let on_drag = on_drag.clone();
let dragging = dragging.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if !phase.bubble() || event.pressed_button != Some(MouseButton::Left) {
return;
}
let Some(edge) = dragging(window, cx) else {
return;
};
on_drag(edge, TouchPhase::Moved, event.position, window, cx);
}
});
window.on_mouse_event({
let on_drag = on_drag.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if !phase.bubble() || event.button != MouseButton::Left {
return;
}
let Some(edge) = dragging(window, cx) else {
return;
};
on_drag(edge, TouchPhase::Ended, event.position, window, cx);
}
});
}
}
impl RenderOnce for SelectionHandles {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let source = self.source;
let on_drag = self.on_drag;
let color = cx.theme().ring();
deferred(
canvas(
{
let source = source.clone();
move |_, window, cx| Self::layout(&source, window, cx)
},
move |_, handles, window, _| {
for handle in &handles {
TouchHandle::paint(handle.edge, handle.caret, color, window);
}
Self::listen(&handles, &source, &on_drag, window);
},
)
.absolute()
.size_0(),
)
.with_priority(POPUP_PRIORITY)
}
}
#[derive(IntoElement)]
struct EditMenu {
id: ElementId,
anchor: Bounds<Pixels>,
items: Vec<EditMenuItem>,
}
impl EditMenu {
fn new(id: impl Into<ElementId>, anchor: Bounds<Pixels>) -> Self {
Self {
id: id.into(),
anchor,
items: Vec::new(),
}
}
fn items(mut self, items: impl IntoIterator<Item = EditMenuItem>) -> Self {
self.items.extend(items);
self
}
}
impl RenderOnce for EditMenu {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let gap = gpui_kit::rems(0.5).to_pixels(window.rem_size());
let height = theme.metrics.control_md;
let radius = theme.radius_full();
let (fill, border, separator, foreground) = (
theme.popover,
theme.popover_border,
theme.popover_separator,
theme.popover_foreground,
);
let shadow = theme.base.shadow.md.clone();
let text = theme.text_control;
let bar_id = self.id.clone();
let items = self.items.into_iter().enumerate().flat_map(|(ix, item)| {
let on_click = item.on_click;
let button = div()
.id(item.label.clone())
.test_support()
.h_full()
.child(
Button::new(ElementId::NamedChild(
bar_id.clone().into(),
item.label.clone(),
))
.ghost()
.tab_stop(false)
.label(item.label)
.h_full()
.px_5()
.rounded(radius)
.text_size(text.size)
.font_weight(FontWeight::NORMAL)
.text_color(foreground)
.on_click(move |_: &ClickEvent, window, cx| on_click(window, cx)),
)
.into_any_element();
(ix > 0)
.then(|| div().w(px(1.)).my_3().bg(separator).into_any_element())
.into_iter()
.chain([button])
});
deferred(
Positioner::side(self.anchor)
.placement(Placement::Top)
.offset(gap)
.occlude()
.child(
div()
.id(self.id)
.flex()
.items_stretch()
.h(height)
.rounded(radius)
.bg(fill)
.border_1()
.border_color(border)
.shadow(shadow)
.children(items),
),
)
.with_priority(POPUP_PRIORITY)
}
}
pub(crate) fn for_state<M: InputModeKind>(
state: &Entity<InputBaseState<M>>,
window: &Window,
cx: &App,
) -> Vec<AnyElement> {
let (capabilities, selectable) = {
let state = state.read(cx);
if state.touch_selection().is_none() {
return Vec::new();
}
let length = state.text().len();
(
state.context_menu_capabilities(),
length > 0 && state.selected_range() != (0..length),
)
};
let editable = capabilities.is_editable();
let copyable = capabilities.is_copyable();
let focus_handle = state.read(cx).presentation().focus_handle().clone();
let dispatch = move |action: &dyn gpui_kit::Action, window: &mut Window, cx: &mut App| {
focus_handle.dispatch_action(action, window, cx);
};
let mut items = Vec::with_capacity(4);
if editable && copyable {
let dispatch = dispatch.clone();
items.push(EditMenuItem::new("Cut", move |window, cx| {
dispatch(&input::Cut, window, cx);
}));
}
if copyable {
let dispatch = dispatch.clone();
let state = state.clone();
items.push(EditMenuItem::new("Copy", move |window, cx| {
dispatch(&input::Copy, window, cx);
state.update(cx, |state, cx| state.close_edit_menu(cx));
}));
}
if editable {
let dispatch = dispatch.clone();
items.push(EditMenuItem::new("Paste", move |window, cx| {
dispatch(&input::Paste, window, cx);
}));
}
if selectable {
let state = state.clone();
items.push(EditMenuItem::new("Select All", move |window, cx| {
state.update(cx, |state, cx| state.select_all_from_edit_menu(window, cx))
}));
}
let drag_state = state.clone();
let source_state = state.clone();
TouchSelectionOverlay::new(
("input-touch-selection", state.entity_id()),
move |_, cx| source_state.read(cx).touch_selection(),
)
.handles(move |edge, phase, position, _, cx| {
drag_state.update(cx, |state, cx| match phase {
TouchPhase::Started => state.begin_edge_drag(edge, position, cx),
TouchPhase::Moved => state.update_edge_drag(position, cx),
TouchPhase::Ended | TouchPhase::Cancelled => state.end_edge_drag(cx),
})
})
.items(items)
.into_elements(window, cx)
}