twrite-gpui 0.9.0

GPUI rendering, canvas text-shaping, and editor component for twrite
Documentation
use gpui::{
    Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ScrollDelta, ScrollWheelEvent, Window,
};
use twrite_core::{HookContext, HookOutcome, Selection};

use super::geometry::find_visible_line;
use super::{Editor, SelectionGranularity};

impl Editor {
    pub(crate) fn handle_mouse_down(
        &mut self,
        event: &MouseDownEvent,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        // A left-click outside the menu dismisses it, then proceeds
        // normally so the click still moves the cursor.
        self.dismiss_context_menu(cx);
        self.focus_handle.focus(window);
        self.reset_blink_cursor(cx);

        if event.click_count == 1
            && !event.modifiers.shift
            && let Some(url) = self.link_at_position(event.position)
        {
            cx.open_url(&url);
            return;
        }

        if event.click_count == 1 && !event.modifiers.shift {
            let clicked = find_visible_line(&self.visible_lines, event.position.y)
                .map(|l| (l.row, l.line_start_byte, l.line_len_bytes));

            if let Some((row, line_start_byte, line_len_bytes)) = clicked
                && self.is_position_over_task_checkbox(event.position, window)
            {
                let offset = self.offset_for_position(event.position, window);
                let col = offset.saturating_sub(line_start_byte).min(line_len_bytes);
                let initial_version = self.buffer.version();
                let mut consumed = false;
                let mut hook_idx = 0;
                while hook_idx < self.hooks.len() {
                    let mut ctx = HookContext::new(
                        &mut self.buffer,
                        &mut self.selection,
                        &mut self.cursor_style,
                        &mut self.prompt,
                        &mut self.pending_effects,
                    );
                    if self.hooks[hook_idx].on_click(&mut ctx, row, col) == HookOutcome::Consumed {
                        consumed = true;
                        break;
                    }
                    hook_idx += 1;
                }
                if consumed {
                    if self.buffer.version() != initial_version {
                        for hook in &mut self.hooks {
                            hook.after_edit(&mut self.buffer);
                        }
                    }
                    for hook in &mut self.hooks {
                        hook.on_selection_change(&self.buffer, self.selection.as_ref());
                    }
                    self.selection = None;
                    self.is_selecting = false;
                    self.flush_effects();
                    cx.notify();
                    return;
                }
            }
        }

        let offset = self.offset_for_position(event.position, window);
        self.is_selecting = true;

        if event.click_count == 2 {
            let word = self.buffer.word_range_at(offset);
            self.selection_granularity = SelectionGranularity::Word;
            self.drag_initial_range = Some(word.clone());
            self.buffer.set_cursor_offset(word.end);
            self.selection = Some(Selection::range(word.start, word.end));
        } else if event.click_count >= 3 {
            let line = self.buffer.line_range_at(offset);
            self.selection_granularity = SelectionGranularity::Line;
            self.drag_initial_range = Some(line.clone());
            self.buffer.set_cursor_offset(line.end);
            self.selection = Some(Selection::range(line.start, line.end));
        } else {
            self.selection_granularity = SelectionGranularity::Character;
            self.drag_initial_range = None;
            self.buffer.set_cursor_offset(offset);

            if event.modifiers.shift {
                if let Some(sel) = self.selection {
                    self.selection = Some(Selection::range(sel.anchor, offset));
                } else {
                    self.selection = Some(Selection::point(offset));
                }
            } else {
                self.selection = Some(Selection::point(offset));
            }
        }

        for hook in &mut self.hooks {
            hook.on_selection_change(&self.buffer, self.selection.as_ref());
        }

        self.scroll_to_cursor(Some(window));
        self.flush_effects();
        cx.notify();
    }

    pub(crate) fn handle_mouse_move(
        &mut self,
        event: &MouseMoveEvent,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if self.is_selecting {
            self.reset_blink_cursor(cx);
            let offset = self.offset_for_position(event.position, window);
            match self.selection_granularity {
                SelectionGranularity::Character => {
                    if self.buffer.cursor_offset() != offset || self.selection.is_none() {
                        self.buffer.set_cursor_offset(offset);

                        if let Some(sel) = self.selection {
                            self.selection = Some(Selection::range(sel.anchor, offset));
                        } else {
                            self.selection = Some(Selection::point(offset));
                        }

                        for hook in &mut self.hooks {
                            hook.on_selection_change(&self.buffer, self.selection.as_ref());
                        }
                        self.scroll_to_cursor(Some(window));
                        cx.notify();
                    }
                }
                SelectionGranularity::Word => {
                    let initial = self.drag_initial_range.clone().unwrap_or(offset..offset);
                    let word = self.buffer.word_range_at(offset);
                    let (anchor, head) = if offset >= initial.start {
                        (initial.start, word.end.max(initial.end))
                    } else {
                        (initial.end, word.start)
                    };
                    let new_sel = Selection::range(anchor, head);
                    if self.selection != Some(new_sel) {
                        self.selection = Some(new_sel);
                        self.buffer.set_cursor_offset(head);
                        for hook in &mut self.hooks {
                            hook.on_selection_change(&self.buffer, self.selection.as_ref());
                        }
                        self.scroll_to_cursor(Some(window));
                        cx.notify();
                    }
                }
                SelectionGranularity::Line => {
                    let initial = self.drag_initial_range.clone().unwrap_or(offset..offset);
                    let line = self.buffer.line_range_at(offset);
                    let (anchor, head) = if offset >= initial.start {
                        (initial.start, line.end.max(initial.end))
                    } else {
                        (initial.end, line.start)
                    };
                    let new_sel = Selection::range(anchor, head);
                    if self.selection != Some(new_sel) {
                        self.selection = Some(new_sel);
                        self.buffer.set_cursor_offset(head);
                        for hook in &mut self.hooks {
                            hook.on_selection_change(&self.buffer, self.selection.as_ref());
                        }
                        self.scroll_to_cursor(Some(window));
                        cx.notify();
                    }
                }
            }
        } else {
            let hovering = self.is_position_over_task_checkbox(event.position, window);
            let hovered_link = self.link_at_position(event.position);
            let link_changed = hovered_link != self.hovered_link;
            let task_changed = hovering != self.is_hovering_task;

            if task_changed || link_changed {
                self.is_hovering_task = hovering;
                self.hovered_link = hovered_link;
                cx.notify();
            }
        }
    }

    pub(crate) fn handle_mouse_up(
        &mut self,
        event: &MouseUpEvent,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.is_selecting = false;
        self.drag_initial_range = None;
        self.selection_granularity = SelectionGranularity::Character;
        if let Some(sel) = self.selection
            && sel.is_empty()
        {
            self.selection = None;
        }
        for hook in &mut self.hooks {
            hook.on_selection_change(&self.buffer, self.selection.as_ref());
        }
        let hovering = self.is_position_over_task_checkbox(event.position, window);
        let hovered_link = self.link_at_position(event.position);
        let link_changed = hovered_link != self.hovered_link;
        let task_changed = hovering != self.is_hovering_task;

        if task_changed || link_changed {
            self.is_hovering_task = hovering;
            self.hovered_link = hovered_link;
        }
        self.flush_effects();
        cx.notify();
    }

    pub(crate) fn handle_scroll_wheel(
        &mut self,
        event: &ScrollWheelEvent,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.dismiss_context_menu(cx);
        self.reset_blink_cursor(cx);
        let delta_lines = match event.delta {
            ScrollDelta::Lines(delta) => -delta.y,
            ScrollDelta::Pixels(delta) => -(delta.y / self.config.line_height),
        };

        let total_lines = self.buffer.len_lines();
        if delta_lines > 0.0 {
            let count = delta_lines.round() as usize;
            self.scroll_row = (self.scroll_row + count).min(total_lines.saturating_sub(1));
        } else if delta_lines < 0.0 {
            let count = (-delta_lines).round() as usize;
            self.scroll_row = self.scroll_row.saturating_sub(count);
        }

        cx.notify();
    }
}