use super::*;
const EDGE_SCROLL_TICK: Duration = Duration::from_millis(16);
const EDGE_SCROLL_MAX: f32 = 48.0;
impl Editor {
pub(super) fn checkbox_at(&self, at: gpui::Point<gpui::Pixels>) -> Option<usize> {
let ix = self.layouts.block_at(at)?;
self.layouts
.checkbox_bounds(ix)
.is_some_and(|bounds| bounds.contains(&at))
.then_some(ix)
}
pub(super) fn append_tail(&mut self, cx: &mut Context<Self>) -> bool {
let Some(last) = self.doc.blocks.len().checked_sub(1) else {
return false;
};
if !self.blocks() {
return false;
}
if self.doc.blocks[last].parts().last() == Some(&Part::Body) {
return false;
}
self.edit(EditKind::Structure, cx, |this| {
this.doc
.blocks
.push(markdown::Block::new(BlockKind::Paragraph(Text::default())));
let ix = this.doc.blocks.len() - 1;
this.selection = Selection::at(Cursor::new(ix, Part::Body, 0).clamp(&this.doc));
vec![]
});
true
}
pub fn press(
&mut self,
position: gpui::Point<gpui::Pixels>,
click_count: usize,
modifiers: gpui::Modifiers,
window: &mut Window,
cx: &mut Context<Self>,
) {
if window.default_prevented() {
return;
}
window.prevent_default();
self.pressed(position, click_count, modifiers, window, cx);
}
pub(super) fn in_drag(&self) -> bool {
self.dragging.is_some()
|| self.lifted.is_some()
|| self.resizing.is_some()
|| self.sizing.is_some()
|| self.table_drag.is_some()
}
pub(super) fn drag_to(&mut self, position: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
self.drag_at = Some(position);
if self.edge_scroll.is_none() && self.edge_step() != 0.0 {
self.edge_scroll = Some(cx.spawn(async move |this, cx| {
loop {
cx.background_executor().timer(EDGE_SCROLL_TICK).await;
let more = this.update(cx, |this, cx| this.edge_scroll_tick(cx));
if !more.unwrap_or(false) {
return;
}
}
}));
}
self.follow(position, cx);
}
fn edge_step(&self) -> f32 {
let (Some(scroll), Some(at)) = (&self.scroll, self.drag_at) else {
return 0.0;
};
if self.dragging.is_none() && self.lifted.is_none() {
return 0.0;
}
let view = scroll.bounds();
let past = if at.y < view.top() {
f32::from(at.y - view.top())
} else if at.y > view.bottom() {
f32::from(at.y - view.bottom())
} else {
return 0.0;
};
(past / 2.0).clamp(-EDGE_SCROLL_MAX, EDGE_SCROLL_MAX)
}
fn edge_scroll_tick(&mut self, cx: &mut Context<Self>) -> bool {
let step = self.edge_step();
let (Some(scroll), Some(at)) = (self.scroll.clone(), self.drag_at) else {
return false;
};
if step == 0.0 {
self.edge_scroll = None;
return false;
}
let offset = scroll.offset();
let y = (offset.y - gpui::px(step)).clamp(-scroll.max_offset().y, gpui::px(0.0));
scroll.set_offset(gpui::point(offset.x, y));
self.follow(at, cx);
cx.notify();
true
}
fn follow(&mut self, position: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
if self.table_drag.is_some() {
return self.drag_table_to(position, cx);
}
if let Some((from, _)) = self.lifted {
if let Some(to) = self.layouts.block_at(position) {
self.lifted = Some((from, to));
cx.notify();
}
return;
}
if self.drag_height(position.y, cx) {
return;
}
if let Some((ix, _)) = self.resizing {
if let Some(width) = self.dragged_width(ix, position.x) {
self.resizing = Some((ix, Some(width)));
cx.notify();
}
return;
}
if let Some((unit, pressed)) = self.dragging.clone()
&& let Some((hit, _)) = self.layouts.hit(position)
{
let (anchor, head) = ui::input::drag_selection(pressed, hit.span(unit, &self.doc));
self.selection = Selection::new(anchor, head).clamp(&self.doc);
cx.notify();
}
}
pub(super) fn pressed(
&mut self,
position: gpui::Point<gpui::Pixels>,
click_count: usize,
modifiers: gpui::Modifiers,
window: &mut Window,
cx: &mut Context<Self>,
) {
ui::popover::close_popup(self, cx, |this| &mut this.block_menu);
ui::popover::close_popup(self, cx, |this| &mut this.language_menu);
ui::popover::close_popup(self, cx, |this| &mut this.table_menu);
ui::popover::close_popup(self, cx, |this| &mut this.text_menu);
ui::popover::close_popup(self, cx, |this| &mut this.image_menu);
self.pasted = None;
self.focus_handle.clone().focus(window, cx);
if let Some(ix) = self.checkbox_at(position) {
self.toggle_task(ix, cx);
return;
}
if self.tail_click(position, cx) {
return;
}
let Some((hit, affinity)) = self.layouts.hit(position) else {
return cx.notify();
};
let unit = ui::input::Granularity::of_clicks(click_count);
self.selection = match modifiers.shift {
true => self.selection.extend_to(hit).with_affinity(affinity),
false => {
let span = hit.span(unit, &self.doc);
let affinity = match span.end == hit {
true => affinity,
false => Affinity::Downstream,
};
Selection::new(span.start, span.end).with_affinity(affinity)
}
}
.clamp(&self.doc);
self.dragging = (!modifiers.shift).then(|| {
let (start, end) = self.selection.ordered();
(unit, start..end)
});
self.history.interrupt();
self.caret_moved();
if let Some(id) = self.anchor_at(position) {
cx.emit(EditorEvent::AnchorActivated(id));
}
cx.notify();
}
pub(super) fn right_pressed(
&mut self,
position: gpui::Point<gpui::Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
ui::popover::close_popup(self, cx, |this| &mut this.block_menu);
ui::popover::close_popup(self, cx, |this| &mut this.language_menu);
if let Some(target) = self.image_target_at(position) {
self.focus_handle.clone().focus(window, cx);
self.image_menu.open(Dropped::new(target, position));
return cx.notify();
}
if let Some((hit, affinity)) = self.layouts.hit(position) {
let (start, end) = self.selection.ordered();
if self.selection.is_collapsed() || hit < start || hit > end {
self.selection = Selection::at(hit).with_affinity(affinity).clamp(&self.doc);
self.history.interrupt();
self.caret_moved();
}
}
self.focus_handle.clone().focus(window, cx);
let items = ui::menu::edit_items(
!self.selection.is_collapsed(),
cx.read_from_clipboard().is_some(),
[&Cut, &Copy, &Paste, &SelectAll],
window,
);
self.text_menu.open(Dropped::new(items, position));
cx.notify();
}
pub(super) fn tail_click(
&mut self,
at: gpui::Point<gpui::Pixels>,
cx: &mut Context<Self>,
) -> bool {
let Some(last) = self.doc.blocks.len().checked_sub(1) else {
return false;
};
let Some(bounds) = self.layouts.block_bounds(last) else {
return false;
};
at.y > bounds.origin.y + bounds.size.height && self.append_tail(cx)
}
}