Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
53bd1a4
Branches
Tags
main
apple-crates
super-sideloader
src
edit_line.rs
Implement super-sideloader ADI backends and IPA loading
Dadoum
commited
53bd1a4
at 2026-07-08 09:55:10
edit_line.rs
Blame
History
Raw
use gpui::{ actions, div, fill, point, prelude::*, px, relative, rgb, rgba, size, App, Bounds, ClipboardItem, Context, CursorStyle, Element, ElementId, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable, GlobalElementId, KeyBinding, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, Window, }; use std::ops::Range; const CONTEXT: &str = "EditLine"; actions!( edit_line, [ Backspace, Delete, Left, Right, SelectLeft, SelectRight, SelectAll, Home, End, Paste, Cut, Copy, ShowCharacterPalette, ] ); pub(crate) fn init(cx: &mut App) { cx.bind_keys([ KeyBinding::new("backspace", Backspace, Some(CONTEXT)), KeyBinding::new("delete", Delete, Some(CONTEXT)), KeyBinding::new("left", Left, Some(CONTEXT)), KeyBinding::new("right", Right, Some(CONTEXT)), KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)), KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)), KeyBinding::new("home", Home, Some(CONTEXT)), KeyBinding::new("end", End, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-v", Paste, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-c", Copy, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-x", Cut, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)), ]); } pub(crate) struct EditLine { focus_handle: FocusHandle, content: SharedString, selected_range: Range<usize>, selection_reversed: bool, marked_range: Option<Range<usize>>, last_layout: Option<ShapedLine>, last_bounds: Option<Bounds<Pixels>>, scroll_x: Pixels, is_selecting: bool, masked: bool, } impl EditLine { pub(crate) fn new(value: impl Into<SharedString>, cx: &mut Context<Self>) -> Self { let content = value.into(); let end = content.len(); Self { focus_handle: cx.focus_handle().tab_stop(true), content, selected_range: end..end, selection_reversed: false, marked_range: None, last_layout: None, last_bounds: None, scroll_x: Pixels::ZERO, is_selecting: false, masked: false, } } pub(crate) fn new_password(value: impl Into<SharedString>, cx: &mut Context<Self>) -> Self { let mut input = Self::new(value, cx); input.masked = true; input } pub(crate) fn value(&self) -> String { self.content.to_string() } pub(crate) fn focus(&self, window: &mut Window, cx: &mut Context<Self>) { self.focus_handle.focus(window, cx); } fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) { if self.selected_range.is_empty() { self.move_to(self.previous_boundary(self.cursor_offset()), cx); } else { self.move_to(self.selected_range.start, cx) } } fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) { if self.selected_range.is_empty() { self.move_to(self.next_boundary(self.selected_range.end), cx); } else { self.move_to(self.selected_range.end, cx) } } fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) { self.select_to(self.previous_boundary(self.cursor_offset()), cx); } fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) { self.select_to(self.next_boundary(self.cursor_offset()), cx); } fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) { self.move_to(0, cx); self.select_to(self.content.len(), cx) } fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) { self.move_to(0, cx); } fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) { self.move_to(self.content.len(), cx); } fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) { if self.selected_range.is_empty() { self.select_to(self.previous_boundary(self.cursor_offset()), cx) } self.replace_text_in_range(None, "", window, cx) } fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) { if self.selected_range.is_empty() { self.select_to(self.next_boundary(self.cursor_offset()), cx) } self.replace_text_in_range(None, "", window, cx) } fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) { if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) { self.replace_text_in_range(None, &text.replace('\n', " "), window, cx); } } fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) { if !self.selected_range.is_empty() { cx.write_to_clipboard(ClipboardItem::new_string( self.content[self.selected_range.clone()].to_string(), )); } } fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) { if !self.selected_range.is_empty() { cx.write_to_clipboard(ClipboardItem::new_string( self.content[self.selected_range.clone()].to_string(), )); self.replace_text_in_range(None, "", window, cx); } } fn show_character_palette( &mut self, _: &ShowCharacterPalette, window: &mut Window, _: &mut Context<Self>, ) { window.show_character_palette(); } fn on_mouse_down( &mut self, event: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>, ) { self.focus_handle.focus(window, cx); self.is_selecting = true; if event.modifiers.shift { self.select_to(self.index_for_mouse_position(event.position), cx); } else { self.move_to(self.index_for_mouse_position(event.position), cx); } } fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, _: &mut Context<Self>) { self.is_selecting = false; } fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) { if self.is_selecting { self.select_to(self.index_for_mouse_position(event.position), cx); } } fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) { self.selected_range = offset..offset; self.selection_reversed = false; cx.notify(); } fn cursor_offset(&self) -> usize { if self.selection_reversed { self.selected_range.start } else { self.selected_range.end } } fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize { if self.content.is_empty() { return 0; } let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref()) else { return 0; }; if position.y < bounds.top() { return 0; } if position.y > bounds.bottom() { return self.content.len(); } line.closest_index_for_x(position.x - bounds.left() + self.scroll_x) } fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) { if self.selection_reversed { self.selected_range.start = offset } else { self.selected_range.end = offset }; if self.selected_range.end < self.selected_range.start { self.selection_reversed = !self.selection_reversed; self.selected_range = self.selected_range.end..self.selected_range.start; } cx.notify(); } fn offset_from_utf16(&self, offset: usize) -> usize { let mut utf8_offset = 0; let mut utf16_count = 0; for ch in self.content.chars() { if utf16_count >= offset { break; } utf16_count += ch.len_utf16(); utf8_offset += ch.len_utf8(); } utf8_offset } fn offset_to_utf16(&self, offset: usize) -> usize { let mut utf16_offset = 0; let mut utf8_count = 0; for ch in self.content.chars() { if utf8_count >= offset { break; } utf8_count += ch.len_utf8(); utf16_offset += ch.len_utf16(); } utf16_offset } fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> { self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end) } fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> { self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end) } fn previous_boundary(&self, offset: usize) -> usize { let offset = snap_to_char_boundary(&self.content, offset); self.content[..offset] .char_indices() .last() .map(|(index, _)| index) .unwrap_or(0) } fn next_boundary(&self, offset: usize) -> usize { let offset = snap_to_char_boundary(&self.content, offset); self.content[offset..] .char_indices() .nth(1) .map(|(index, _)| offset + index) .unwrap_or(self.content.len()) } } impl EntityInputHandler for EditLine { fn text_for_range( &mut self, range_utf16: Range<usize>, actual_range: &mut Option<Range<usize>>, _: &mut Window, _: &mut Context<Self>, ) -> Option<String> { let range = self.range_from_utf16(&range_utf16); actual_range.replace(self.range_to_utf16(&range)); Some(self.content[range].to_string()) } fn selected_text_range( &mut self, _: bool, _: &mut Window, _: &mut Context<Self>, ) -> Option<UTF16Selection> { Some(UTF16Selection { range: self.range_to_utf16(&self.selected_range), reversed: self.selection_reversed, }) } fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> { self.marked_range .as_ref() .map(|range| self.range_to_utf16(range)) } fn unmark_text(&mut self, _: &mut Window, _: &mut Context<Self>) { self.marked_range = None; } fn replace_text_in_range( &mut self, range_utf16: Option<Range<usize>>, new_text: &str, _: &mut Window, cx: &mut Context<Self>, ) { let range = range_utf16 .as_ref() .map(|range_utf16| self.range_from_utf16(range_utf16)) .or(self.marked_range.clone()) .unwrap_or(self.selected_range.clone()); self.content = (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..]) .into(); self.selected_range = range.start + new_text.len()..range.start + new_text.len(); self.selection_reversed = false; self.marked_range.take(); cx.notify(); } fn replace_and_mark_text_in_range( &mut self, range_utf16: Option<Range<usize>>, new_text: &str, new_selected_range_utf16: Option<Range<usize>>, _: &mut Window, cx: &mut Context<Self>, ) { let range = range_utf16 .as_ref() .map(|range_utf16| self.range_from_utf16(range_utf16)) .or(self.marked_range.clone()) .unwrap_or(self.selected_range.clone()); self.content = (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..]) .into(); self.marked_range = (!new_text.is_empty()).then_some(range.start..range.start + new_text.len()); self.selected_range = new_selected_range_utf16 .as_ref() .map(|range_utf16| self.range_from_utf16(range_utf16)) .map(|new_range| new_range.start + range.start..new_range.end + range.end) .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len()); self.selection_reversed = false; cx.notify(); } fn bounds_for_range( &mut self, range_utf16: Range<usize>, bounds: Bounds<Pixels>, _: &mut Window, _: &mut Context<Self>, ) -> Option<Bounds<Pixels>> { let last_layout = self.last_layout.as_ref()?; let range = self.range_from_utf16(&range_utf16); Some(Bounds::from_corners( point( bounds.left() + last_layout.x_for_index(range.start) - self.scroll_x, bounds.top(), ), point( bounds.left() + last_layout.x_for_index(range.end) - self.scroll_x, bounds.bottom(), ), )) } fn character_index_for_point( &mut self, point: Point<Pixels>, _: &mut Window, _: &mut Context<Self>, ) -> Option<usize> { let line_point = self.last_bounds?.localize(&point)?; let last_layout = self.last_layout.as_ref()?; let utf8_index = last_layout.index_for_x(line_point.x + self.scroll_x)?; Some(self.offset_to_utf16(utf8_index)) } } impl Render for EditLine { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { div() .key_context(CONTEXT) .track_focus(&self.focus_handle(cx)) .cursor(CursorStyle::IBeam) .on_action(cx.listener(Self::backspace)) .on_action(cx.listener(Self::delete)) .on_action(cx.listener(Self::left)) .on_action(cx.listener(Self::right)) .on_action(cx.listener(Self::select_left)) .on_action(cx.listener(Self::select_right)) .on_action(cx.listener(Self::select_all)) .on_action(cx.listener(Self::home)) .on_action(cx.listener(Self::end)) .on_action(cx.listener(Self::paste)) .on_action(cx.listener(Self::cut)) .on_action(cx.listener(Self::copy)) .on_action(cx.listener(Self::show_character_palette)) .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up)) .on_mouse_move(cx.listener(Self::on_mouse_move)) .min_w_0() .w_full() .child(EditLineElement { input: cx.entity() }) } } impl Focusable for EditLine { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } struct EditLineElement { input: Entity<EditLine>, } struct PrepaintState { line: Option<ShapedLine>, cursor: Option<PaintQuad>, selection: Option<PaintQuad>, scroll_x: Pixels, } impl IntoElement for EditLineElement { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for EditLineElement { type RequestLayoutState = (); type PrepaintState = PrepaintState; fn id(&self) -> Option<ElementId> { None } fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { None } fn request_layout( &mut self, _: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); style.size.width = relative(1.).into(); style.size.height = window.line_height().into(); (window.request_layout(style, [], cx), ()) } fn prepaint( &mut self, _: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, bounds: Bounds<Pixels>, _: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { let input = self.input.read(cx); let content = if input.masked { "*".repeat(input.content.len()).into() } else { input.content.clone() }; let selected_range = input.selected_range.clone(); let cursor = input.cursor_offset(); let previous_scroll_x = input.scroll_x; let style = window.text_style(); let run = TextRun { len: content.len(), font: style.font(), color: style.color, background_color: None, underline: None, strikethrough: None, }; let runs = if let Some(marked_range) = input.marked_range.as_ref() { vec![ TextRun { len: marked_range.start, ..run.clone() }, TextRun { len: marked_range.end - marked_range.start, underline: Some(UnderlineStyle { color: Some(run.color), thickness: px(1.), wavy: false, }), ..run.clone() }, TextRun { len: content.len() - marked_range.end, ..run }, ] .into_iter() .filter(|run| run.len > 0) .collect() } else { vec![run] }; let font_size = style.font_size.to_pixels(window.rem_size()); let line = window .text_system() .shape_line(content, font_size, &runs, None); let cursor_pos = line.x_for_index(cursor); let scroll_x = adjusted_scroll_x( previous_scroll_x, line.width(), bounds.size.width, cursor_pos, ); let (selection, cursor) = if selected_range.is_empty() { ( None, Some(fill( Bounds::new( point(bounds.left() + cursor_pos - scroll_x, bounds.top() + px(2.)), size(px(1.), bounds.bottom() - bounds.top() - px(4.)), ), rgb(0x173f45), )), ) } else { ( Some(fill( Bounds::from_corners( point( bounds.left() + line.x_for_index(selected_range.start) - scroll_x, bounds.top() + px(1.), ), point( bounds.left() + line.x_for_index(selected_range.end) - scroll_x, bounds.bottom() - px(1.), ), ), rgba(0x33298791), )), None, ) }; PrepaintState { line: Some(line), cursor, selection, scroll_x, } } fn paint( &mut self, _: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, bounds: Bounds<Pixels>, _: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { let focus_handle = self.input.read(cx).focus_handle.clone(); window.handle_input( &focus_handle, ElementInputHandler::new(bounds, self.input.clone()), cx, ); if let Some(selection) = prepaint.selection.take() { window.paint_quad(selection); } let line = prepaint.line.take().unwrap(); let scroll_x = prepaint.scroll_x; line.paint( point(bounds.origin.x - scroll_x, bounds.origin.y), window.line_height(), gpui::TextAlign::Left, None, window, cx, ) .unwrap(); if focus_handle.is_focused(window) { if let Some(cursor) = prepaint.cursor.take() { window.paint_quad(cursor); } } self.input.update(cx, |input, _| { input.last_layout = Some(line); input.last_bounds = Some(bounds); input.scroll_x = scroll_x; }); } } fn adjusted_scroll_x( previous: Pixels, line_width: Pixels, visible_width: Pixels, cursor_x: Pixels, ) -> Pixels { let padding = px(4.); if line_width.as_f32() <= visible_width.as_f32() { return Pixels::ZERO; } let mut scroll_x = previous; let right_edge = scroll_x + visible_width - padding; if cursor_x.as_f32() > right_edge.as_f32() { scroll_x = cursor_x - visible_width + padding; } let left_edge = scroll_x + padding; if cursor_x.as_f32() < left_edge.as_f32() { scroll_x = cursor_x - padding; } let max_scroll = line_width - visible_width + padding; if scroll_x.as_f32() < 0. { Pixels::ZERO } else if scroll_x.as_f32() > max_scroll.as_f32() { max_scroll } else { scroll_x } } fn snap_to_char_boundary(value: &str, mut cursor: usize) -> usize { cursor = cursor.min(value.len()); while !value.is_char_boundary(cursor) { cursor -= 1; } cursor }