/
githubmirror
/
lapce
Обзор
Документация
Войти
/
githubmirror
/
lapce
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
lapce-app/src/text_input.rs
801 строка
26 KB
Jakub Panek
chore: bump floem
30 авг 2025, 06:56
Не верифицирован
30 авг 2025, 06:56
ba703dd
Код
Авторство
О чём код?
use std::{rc::Rc, sync::Arc}; use floem::{ Renderer, View, ViewId, action::{set_ime_allowed, set_ime_cursor_area}, context::EventCx, event::{Event, EventListener, EventPropagation}, kurbo::Stroke, peniko::{ Color, kurbo::{Line, Point, Rect, Size, Vec2}, }, prop_extractor, reactive::{ Memo, ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, create_effect, create_memo, create_rw_signal, }, style::{ CursorStyle, FontFamily, FontSize, FontStyle, FontWeight, LineHeight, PaddingLeft, Style, TextColor, }, taffy::prelude::NodeId, text::{Attrs, AttrsList, FamilyOwned, TextLayout}, unit::PxPct, views::Decorators, }; use lapce_core::{ buffer::rope_text::RopeText, cursor::{Cursor, CursorMode}, selection::Selection, }; use lapce_xi_rope::Rope; use crate::{ config::{LapceConfig, color::LapceColor}, doc::Doc, editor::{DocSignal, EditorData, view::editor_style}, keypress::KeyPressFocus, main_split::Editors, window_tab::CommonData, }; prop_extractor! { Extractor { color: TextColor, font_size: FontSize, font_family: FontFamily, font_weight: FontWeight, font_style: FontStyle, line_height: LineHeight, } } /// Builder for creating a [`TextInput`] easily. pub struct TextInputBuilder { is_focused: Option<Memo<bool>>, // TODO: it'd be nice to not need to box this key_focus: Option<Box<dyn KeyPressFocus>>, value: Option<Rope>, keyboard_focus: RwSignal<bool>, } impl Default for TextInputBuilder { fn default() -> Self { Self::new() } } impl TextInputBuilder { pub fn new() -> Self { Self { is_focused: None, key_focus: None, value: None, keyboard_focus: create_rw_signal(false), } } pub fn is_focused(mut self, is_focused: impl Fn() -> bool + 'static) -> Self { let keyboard_focus = self.keyboard_focus; self.is_focused = Some(create_memo(move |_| is_focused() || keyboard_focus.get())); self } /// Initialize with a specific value. /// If this is set it will apply the value via reloading the editor's doc as pristine. pub fn value(mut self, value: impl Into<Rope>) -> Self { self.value = Some(value.into()); self } pub fn key_focus(mut self, key_focus: impl KeyPressFocus + 'static) -> Self { self.key_focus = Some(Box::new(key_focus)); self } pub fn build( self, cx: Scope, editors: Editors, common: Rc<CommonData>, ) -> TextInput { let editor = editors.make_local(cx, common); let id = editor.id(); self.build_editor(editor).on_cleanup(move || { editors.remove(id); }) } /// Build the text input with a specific editor. /// This function does *not* perform add/cleanup the editor to/from [`Editors`] pub fn build_editor(self, editor: EditorData) -> TextInput { let keyboard_focus = self.keyboard_focus; let is_focused = if let Some(is_focused) = self.is_focused { is_focused } else { create_memo(move |_| keyboard_focus.get()) }; if let Some(value) = self.value { editor.doc().reload(value, true); } text_input_full(editor, self.key_focus, is_focused, keyboard_focus) } } /// Create a basic single line text input /// `e_data` is the editor data that this input is associated with. /// `supplied_editor` /// `key_focus` is what receives the keydown events, leave as `None` to default to editor. /// `is_focused` is a function that returns if the input is focused, used for certain events. fn text_input_full<T: KeyPressFocus + 'static>( e_data: EditorData, key_focus: Option<T>, is_focused: Memo<bool>, keyboard_focus: RwSignal<bool>, ) -> TextInput { let id = ViewId::new(); let doc = e_data.doc_signal(); let cursor = e_data.cursor(); let config = e_data.common.config; let keypress = e_data.common.keypress; let window_origin = create_rw_signal(Point::ZERO); let cursor_line = create_rw_signal(Line::new(Point::ZERO, Point::ZERO)); let local_editor = e_data.clone(); let editor = local_editor.editor.clone(); { let doc = doc.get(); create_effect(move |_| { let offset = cursor.with(|c| c.offset()); let (content, offset, preedit_range) = { let content = doc.buffer.with(|b| b.to_string()); if let Some(preedit) = doc.preedit.preedit.get().as_ref() { let mut new_content = String::new(); new_content.push_str(&content[..offset]); new_content.push_str(&preedit.text); new_content.push_str(&content[offset..]); let range = (offset, offset + preedit.text.len()); let offset = preedit .cursor .as_ref() .map(|(_, end)| offset + *end) .unwrap_or(offset); (new_content, offset, Some(range)) } else { (content, offset, None) } }; id.update_state(TextInputState::Content { text: content, offset, preedit_range, }); }); } { create_effect(move |_| { let focus = is_focused.get(); id.update_state(TextInputState::Focus(focus)); }); let editor = editor.clone(); let ime_allowed = editor.ime_allowed; create_effect(move |_| { let focus = is_focused.get(); if focus { if !ime_allowed.get_untracked() { ime_allowed.set(true); set_ime_allowed(true); } let cursor_line = cursor_line.get(); let window_origin = window_origin.get(); let viewport = editor.viewport.get(); let origin = window_origin + Vec2::new( cursor_line.p1.x - viewport.x0, cursor_line.p1.y - viewport.y0, ); set_ime_cursor_area(origin, Size::new(800.0, 600.0)); } }); } let common_keyboard_focus = e_data.common.keyboard_focus; let ed1 = editor.clone(); let ed2 = editor.clone(); TextInput { id, config, offset: 0, preedit_range: None, layout_rect: Rect::ZERO, content: "".to_string(), focus: false, text_node: None, text_layout: create_rw_signal(None), text_rect: Rect::ZERO, text_viewport: Rect::ZERO, cursor_line, placeholder: "".to_string(), placeholder_text_layout: None, editor: e_data.clone(), cursor_pos: Point::ZERO, on_cursor_pos: None, hide_cursor: editor.cursor_info.hidden, style: Default::default(), } .style(move |s| { editor_style(config, doc, s) .cursor(CursorStyle::Text) .padding_horiz(10.0) .padding_vert(6.0) }) .on_move(move |pos| { window_origin.set(pos); }) .on_event_stop(EventListener::FocusGained, move |_| { keyboard_focus.set(true); common_keyboard_focus.set(Some(id)); }) .on_event_stop(EventListener::FocusLost, move |_| { keyboard_focus.set(false); if common_keyboard_focus.get_untracked() == Some(id) { common_keyboard_focus.set(None); } }) .on_event(EventListener::KeyDown, move |event| { if let Event::KeyDown(key_event) = event { let keypress = keypress.get_untracked(); let key_focus = key_focus .as_ref() .map(|k| k as &dyn KeyPressFocus) .unwrap_or(&e_data); if keypress.key_down(key_event, key_focus).handled { EventPropagation::Stop } else { EventPropagation::Continue } } else { EventPropagation::Continue } }) .on_event(EventListener::ImePreedit, move |event| { if !is_focused.get_untracked() { return EventPropagation::Continue; } if let Event::ImePreedit { text, cursor: ime_cursor, } = event { if text.is_empty() { ed1.clear_preedit(); } else { let offset = cursor.with_untracked(|c| c.offset()); ed1.set_preedit(text.clone(), ime_cursor.to_owned(), offset); } } EventPropagation::Stop }) .on_event(EventListener::ImeCommit, move |event| { if !is_focused.get_untracked() { return EventPropagation::Continue; } if let Event::ImeCommit(text) = event { ed2.clear_preedit(); ed2.receive_char(text.as_str()); } EventPropagation::Stop }) } enum TextInputState { Content { text: String, offset: usize, preedit_range: Option<(usize, usize)>, }, Focus(bool), Placeholder(String), } pub struct TextInput { id: ViewId, content: String, offset: usize, preedit_range: Option<(usize, usize)>, editor: EditorData, focus: bool, text_node: Option<NodeId>, text_layout: RwSignal<Option<TextLayout>>, text_rect: Rect, text_viewport: Rect, layout_rect: Rect, cursor_line: RwSignal<Line>, placeholder: String, placeholder_text_layout: Option<TextLayout>, cursor_pos: Point, on_cursor_pos: Option<Box<dyn Fn(Point)>>, hide_cursor: RwSignal<bool>, config: ReadSignal<Arc<LapceConfig>>, style: Extractor, } impl TextInput { pub fn placeholder(self, placeholder: impl Fn() -> String + 'static) -> Self { let id = self.id; create_effect(move |_| { let placeholder = placeholder(); id.update_state(TextInputState::Placeholder(placeholder)); }); self } pub fn on_cursor_pos(mut self, cursor_pos: impl Fn(Point) + 'static) -> Self { self.on_cursor_pos = Some(Box::new(cursor_pos)); self } pub fn editor(&self) -> EditorData { self.editor.clone() } pub fn doc_signal(&self) -> DocSignal { self.editor.doc_signal() } pub fn doc(&self) -> Rc<Doc> { self.editor.doc() } pub fn cursor(&self) -> RwSignal<Cursor> { self.editor.cursor() } fn set_text_layout(&mut self) { let mut text_layout = TextLayout::new(); let mut attrs = Attrs::new().color(self.style.color().unwrap_or(Color::BLACK)); if let Some(font_size) = self.style.font_size() { attrs = attrs.font_size(font_size); } if let Some(font_style) = self.style.font_style() { attrs = attrs.style(font_style); } let font_family = self.style.font_family().as_ref().map(|font_family| { let family: Vec<FamilyOwned> = FamilyOwned::parse_list(font_family).collect(); family }); if let Some(font_family) = font_family.as_ref() { attrs = attrs.family(font_family); } if let Some(font_weight) = self.style.font_weight() { attrs = attrs.weight(font_weight); } if let Some(line_height) = self.style.line_height() { attrs = attrs.line_height(line_height); } text_layout.set_text( if self.content.is_empty() { " " } else { self.content.as_str() }, AttrsList::new(attrs.clone()), None, ); self.text_layout.set(Some(text_layout)); let mut placeholder_text_layout = TextLayout::new(); attrs = attrs.color( self.style .color() .unwrap_or(Color::BLACK) .multiply_alpha(0.5), ); placeholder_text_layout.set_text( &self.placeholder, AttrsList::new(attrs), None, ); self.placeholder_text_layout = Some(placeholder_text_layout); } fn hit_index(&self, _cx: &mut EventCx, point: Point) -> usize { self.text_layout.with_untracked(|text_layout| { if let Some(text_layout) = text_layout.as_ref() { let padding_left = match self.id.get_combined_style().get(PaddingLeft) { PxPct::Px(v) => v, PxPct::Pct(pct) => { let layout = self.id.get_layout().unwrap_or_default(); pct * layout.size.width as f64 } }; let hit = text_layout.hit_point(Point::new(point.x - padding_left, 0.0)); hit.index.min(self.content.len()) } else { 0 } }) } fn clamp_text_viewport(&mut self, text_viewport: Rect) { let text_rect = self.text_rect; let actual_size = text_rect.size(); let width = text_rect.width(); let height = text_rect.height(); let child_size = self .text_layout .with_untracked(|text_layout| text_layout.as_ref().unwrap().size()); let mut text_viewport = text_viewport; if width >= child_size.width { text_viewport.x0 = 0.0; } else if text_viewport.x0 > child_size.width - width { text_viewport.x0 = child_size.width - width; } else if text_viewport.x0 < 0.0 { text_viewport.x0 = 0.0; } if height >= child_size.height { text_viewport.y0 = 0.0; } else if text_viewport.y0 > child_size.height - height { text_viewport.y0 = child_size.height - height; } else if text_viewport.y0 < 0.0 { text_viewport.y0 = 0.0; } let text_viewport = text_viewport.with_size(actual_size); if text_viewport != self.text_viewport { self.text_viewport = text_viewport; self.id.request_paint(); } } fn ensure_cursor_visible(&mut self) { fn closest_on_axis(val: f64, min: f64, max: f64) -> f64 { assert!(min <= max); if val > min && val < max { 0.0 } else if val <= min { val - min } else { val - max } } let rect = Rect::ZERO.with_origin(self.cursor_pos).inflate(10.0, 0.0); // clamp the target region size to our own size. // this means we will show the portion of the target region that // includes the origin. let target_size = Size::new( rect.width().min(self.text_viewport.width()), rect.height().min(self.text_viewport.height()), ); let rect = rect.with_size(target_size); let x0 = closest_on_axis( rect.min_x(), self.text_viewport.min_x(), self.text_viewport.max_x(), ); let x1 = closest_on_axis( rect.max_x(), self.text_viewport.min_x(), self.text_viewport.max_x(), ); let y0 = closest_on_axis( rect.min_y(), self.text_viewport.min_y(), self.text_viewport.max_y(), ); let y1 = closest_on_axis( rect.max_y(), self.text_viewport.min_y(), self.text_viewport.max_y(), ); let delta_x = if x0.abs() > x1.abs() { x0 } else { x1 }; let delta_y = if y0.abs() > y1.abs() { y0 } else { y1 }; let new_origin = self.text_viewport.origin() + Vec2::new(delta_x, delta_y); self.clamp_text_viewport(self.text_viewport.with_origin(new_origin)); } } impl View for TextInput { fn id(&self) -> ViewId { self.id } fn update( &mut self, _cx: &mut floem::context::UpdateCx, state: Box<dyn std::any::Any>, ) { if let Ok(state) = state.downcast() { match *state { TextInputState::Content { text, offset, preedit_range, } => { self.content = text; self.offset = offset; self.preedit_range = preedit_range; self.text_layout.set(None); } TextInputState::Focus(focus) => { self.focus = focus; } TextInputState::Placeholder(placeholder) => { self.placeholder = placeholder; self.placeholder_text_layout = None; } } self.id.request_layout(); } } fn style_pass(&mut self, cx: &mut floem::context::StyleCx<'_>) { if self.style.read(cx) { self.set_text_layout(); self.id.request_layout(); } } fn layout( &mut self, cx: &mut floem::context::LayoutCx, ) -> floem::taffy::prelude::NodeId { cx.layout_node(self.id, true, |_cx| { if self .text_layout .with_untracked(|text_layout| text_layout.is_none()) || self.placeholder_text_layout.is_none() { self.set_text_layout(); } let text_layout = self.text_layout; text_layout.with_untracked(|text_layout| { let text_layout = text_layout.as_ref().unwrap(); let offset = self.cursor().get_untracked().offset(); let cursor_point = text_layout.hit_position(offset).point; if cursor_point != self.cursor_pos { self.cursor_pos = cursor_point; self.ensure_cursor_visible(); } let size = text_layout.size(); let height = size.height as f32; if self.text_node.is_none() { self.text_node = Some(self.id.new_taffy_node()); } let text_node = self.text_node.unwrap(); let style = Style::new().height(height).to_taffy_style(); self.id.set_taffy_style(text_node, style); }); vec![self.text_node.unwrap()] }) } fn compute_layout( &mut self, _cx: &mut floem::context::ComputeLayoutCx, ) -> Option<Rect> { let layout = self.id.get_layout().unwrap_or_default(); let style = self.id.get_combined_style(); let style = style.builtin(); let padding_left = match style.padding_left() { PxPct::Px(padding) => padding, PxPct::Pct(pct) => pct * layout.size.width as f64, }; let padding_right = match style.padding_right() { PxPct::Px(padding) => padding, PxPct::Pct(pct) => pct * layout.size.width as f64, }; let size = Size::new(layout.size.width as f64, layout.size.height as f64); let mut text_rect = size.to_rect(); text_rect.x0 += padding_left; text_rect.x1 -= padding_right; self.text_rect = text_rect; self.clamp_text_viewport(self.text_viewport); let text_node = self.text_node.unwrap(); let location = self.id.taffy_layout(text_node).unwrap_or_default().location; self.layout_rect = size .to_rect() .with_origin(Point::new(location.x as f64, location.y as f64)); let offset = self.cursor().with_untracked(|c| c.offset()); let cursor_line = self.text_layout.with_untracked(|text_layout| { let hit_position = text_layout.as_ref().unwrap().hit_position(offset); let point = Point::new(location.x as f64, location.y as f64) - self.text_viewport.origin().to_vec2(); let cursor_point = hit_position.point + point.to_vec2(); Line::new( Point::new( cursor_point.x, cursor_point.y - hit_position.glyph_ascent, ), Point::new( cursor_point.x, cursor_point.y + hit_position.glyph_descent, ), ) }); self.cursor_line.set(cursor_line); None } fn event_before_children( &mut self, cx: &mut floem::context::EventCx, event: &floem::event::Event, ) -> EventPropagation { let text_offset = self.text_viewport.origin(); let event = event.clone().offset((-text_offset.x, -text_offset.y)); match event { Event::PointerDown(pointer) => { let offset = self.hit_index(cx, pointer.pos); self.cursor().update(|cursor| { cursor.set_insert(Selection::caret(offset)); }); if pointer.button.is_primary() && pointer.count == 2 { let offset = self.hit_index(cx, pointer.pos); let (start, end) = self .doc() .buffer .with_untracked(|buffer| buffer.select_word(offset)); self.cursor().update(|cursor| { cursor.set_insert(Selection::region(start, end)); }); } else if pointer.button.is_primary() && pointer.count == 3 { self.cursor().update(|cursor| { cursor.set_insert(Selection::region(0, self.content.len())); }); } cx.update_active(self.id); } Event::PointerMove(pointer) => { if cx.is_active(self.id) { let offset = self.hit_index(cx, pointer.pos); self.cursor().update(|cursor| { cursor.set_offset(offset, true, false); }); } } Event::PointerWheel(pointer_event) => { let delta = pointer_event.delta; let delta = if delta.x == 0.0 && delta.y != 0.0 { Vec2::new(delta.y, delta.x) } else { delta }; self.clamp_text_viewport(self.text_viewport + delta); return EventPropagation::Continue; } _ => {} } EventPropagation::Continue } fn paint(&mut self, cx: &mut floem::context::PaintCx) { cx.save(); cx.clip(&self.text_rect.inflate(1.0, 0.0)); let text_node = self.text_node.unwrap(); let location = self.id.taffy_layout(text_node).unwrap_or_default().location; let point = Point::new(location.x as f64, location.y as f64) - self.text_viewport.origin().to_vec2(); self.text_layout.with_untracked(|text_layout| { let text_layout = text_layout.as_ref().unwrap(); let height = text_layout.size().height; let config = self.config.get_untracked(); let cursor = self.cursor().get_untracked(); if let CursorMode::Insert(selection) = &cursor.mode { for region in selection.regions() { if !region.is_caret() { let min = text_layout.hit_position(region.min()).point.x; let max = text_layout.hit_position(region.max()).point.x; cx.fill( &Rect::ZERO .with_size(Size::new(max - min, height)) .with_origin(Point::new(min + point.x, point.y)), config.color(LapceColor::EDITOR_SELECTION), 0.0, ); } } } if !self.content.is_empty() { cx.draw_text(text_layout, point); } else if !self.placeholder.is_empty() { cx.draw_text(self.placeholder_text_layout.as_ref().unwrap(), point); } if let Some((start, end)) = self.preedit_range { let start_position = text_layout.hit_position(start); let start_point = start_position.point + self.layout_rect.origin().to_vec2() - self.text_viewport.origin().to_vec2(); let end_position = text_layout.hit_position(end); let end_point = end_position.point + self.layout_rect.origin().to_vec2() - self.text_viewport.origin().to_vec2(); let line = Line::new( Point::new( start_point.x, start_point.y + start_position.glyph_descent, ), Point::new( end_point.x, end_point.y + end_position.glyph_descent, ), ); cx.stroke( &line, config.color(LapceColor::EDITOR_FOREGROUND), &Stroke::new(1.0), ); } if !self.hide_cursor.get_untracked() && (self.focus || cx.is_focused(self.id)) { cx.clip(&self.text_rect.inflate(2.0, 2.0)); let hit_position = text_layout.hit_position(self.offset); let cursor_point = hit_position.point + self.layout_rect.origin().to_vec2() - self.text_viewport.origin().to_vec2(); let line = Line::new( Point::new( cursor_point.x, cursor_point.y - hit_position.glyph_ascent, ), Point::new( cursor_point.x, cursor_point.y + hit_position.glyph_descent, ), ); cx.stroke( &line, self.config.get_untracked().color(LapceColor::EDITOR_CARET), &Stroke::new(2.0), ); } cx.restore(); }); } }