//! Text editors display a multi-line text input for text editing. //! //! # Example //! ```no_run //! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } //! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; //! # //! use iced::widget::text_editor; //! //! struct State { //! content: text_editor::Content, //! } //! //! #[derive(Debug, Clone)] //! enum Message { //! Edit(text_editor::Action) //! } //! //! fn view(state: &State) -> Element<'_, Message> { //! text_editor(&state.content) //! .placeholder("Type something here...") //! .on_action(Message::Edit) //! .into() //! } //! //! fn update(state: &mut State, message: Message) { //! match message { //! Message::Edit(action) => { //! state.content.perform(action); //! } //! } //! } //! ``` use iced_wgpu::core::alignment; use iced_wgpu::core::clipboard::{self, Clipboard}; use iced_wgpu::core::input_method; use iced_wgpu::core::keyboard; use iced_wgpu::core::keyboard::key; use iced_wgpu::core::layout::{self, Layout}; use iced_wgpu::core::mouse; use iced_wgpu::core::renderer::{self, Renderer as _}; use iced_wgpu::core::text::editor::Editor as _; use iced_wgpu::core::text::Renderer as _; use iced_wgpu::core::text::highlighter::{self, Highlighter}; use iced_wgpu::core::text::paragraph::Paragraph as _; use iced_wgpu::core::text::{self, LineHeight, Span, Text, Wrapping}; use iced_wgpu::core::theme; use iced_wgpu::core::time::{Duration, Instant}; use iced_wgpu::core::widget::operation; use iced_wgpu::core::widget::{self, Widget}; use iced_wgpu::core::window; use iced_wgpu::core::{ Background, Border, Color, Element, Event, Font, InputMethod, Length, Padding, Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, }; use std::borrow::Cow; use std::cell::RefCell; use std::fmt; use std::ops; use std::ops::DerefMut; use std::sync::Arc; pub use text::editor::{ Action, Cursor, Edit, Line, LineEnding, Motion, Position, Selection, }; /// An anchored child element rendered at a line boundary within the text widget. /// The caller builds these using existing rendering code; the widget just draws them in order. pub struct AnchoredItem<'a, Message, Theme = iced_wgpu::core::Theme> { pub after_line: usize, pub height: f32, pub element: Element<'a, Message, Theme, iced_wgpu::Renderer>, } /// Per-logical-line metrics. Stored on State so layout publishes once /// and every consumer (draw, cursor, hit-test) reads the same data. #[derive(Clone, Default, Debug)] pub struct LineMetric { /// Widget-y of this line's first visual row (relative to text_bounds.y). pub widget_y: f32, /// Cosmic-text's viewport-relative y of this line's first visual row — /// matches the y produced by `Selection::Caret(position).y` and what /// `Action::Click { y }` consumes (already scroll-adjusted, items /// invisible to it). Diverges from `widget_y` whenever an anchored /// item sits between this line and `scroll.line`. pub viewport_y: f32, /// Number of visual rows this logical line occupies after wrap. pub visual_rows: usize, } /// Translate a cosmic-reported y (`Selection::Caret`, `Selection::Range`) /// into our widget-y so cursor + selection rectangles draw on top of the /// text rows the compositor actually rendered. fn cosmic_y_to_widget_y(metrics: &[LineMetric], cosmic_y: f32, _line_h: f32) -> f32 { if metrics.is_empty() { return cosmic_y; } for i in (0..metrics.len() - 1).rev() { if metrics[i].viewport_y <= cosmic_y { return metrics[i].widget_y + (cosmic_y - metrics[i].viewport_y); } } metrics[0].widget_y + (cosmic_y - metrics[0].viewport_y) } /// Translate a widget-y (mouse coords) back into the y cosmic-text expects /// for click/drag actions — the inverse of `cosmic_y_to_widget_y`. fn widget_y_to_cosmic_y(metrics: &[LineMetric], widget_y: f32, line_h: f32) -> f32 { if metrics.len() < 2 { return widget_y; } let line_count = metrics.len() - 1; for i in 0..line_count { let line_top = metrics[i].widget_y; let line_bot = line_top + metrics[i].visual_rows as f32 * line_h; if widget_y < line_bot { if widget_y < line_top { return metrics[i].viewport_y; } return metrics[i].viewport_y + (widget_y - line_top); } let next_top = metrics[i + 1].widget_y; if widget_y < next_top { return metrics[i].viewport_y + metrics[i].visual_rows as f32 * line_h; } } let tail = metrics.last().unwrap(); tail.viewport_y + (widget_y - tail.widget_y).max(0.0) } /// Distance-driven fade ratio for the gutter rainbow. `0.0` at the cursor /// (full saturation), `1.0` at the far end of the fade window. const GUTTER_FADE_CYCLES: f32 = 2.5; fn gutter_fade_t(distance: usize) -> f32 { let max_d = GUTTER_FADE_CYCLES * crate::syntax::USER_IDENT_PALETTE_SIZE as f32; (distance as f32 / max_d).min(1.0) } /// builds iced Spans from cosmic glyphs, grouping by (color, weight, style) and skipping marker ranges. fn build_color_spans<'a>( text: &'a str, glyphs: &[cosmic_text::LayoutGlyph], attrs_list: &cosmic_text::AttrsList, base_font: Font, font_size_px: f32, marker_ranges: &[std::ops::Range], ) -> Vec> { use iced_wgpu::core::font::{Style as IcedStyle, Weight as IcedWeight}; fn cosmic_to_iced(c: cosmic_text::Color, font_size_px: f32) -> Color { let raw = Color::from_rgba8(c.r(), c.g(), c.b(), c.a() as f32 / 255.0); crate::oklab::lighten_for_size(raw, font_size_px) } fn cosmic_to_iced_weight(w: cosmic_text::Weight) -> IcedWeight { match w.0 { 0..=149 => IcedWeight::Thin, 150..=249 => IcedWeight::ExtraLight, 250..=349 => IcedWeight::Light, 350..=449 => IcedWeight::Normal, 450..=549 => IcedWeight::Medium, 550..=649 => IcedWeight::Semibold, 650..=749 => IcedWeight::Bold, 750..=849 => IcedWeight::ExtraBold, _ => IcedWeight::Black, } } fn cosmic_to_iced_style(s: cosmic_text::Style) -> IcedStyle { match s { cosmic_text::Style::Normal => IcedStyle::Normal, cosmic_text::Style::Italic => IcedStyle::Italic, cosmic_text::Style::Oblique => IcedStyle::Oblique, } } /// span grouping key — color repr plus iced weight/style. type StyleKey = (Option, IcedWeight, IcedStyle); let style_at = |byte_idx: usize| -> (IcedWeight, IcedStyle) { let attrs = attrs_list.get_span(byte_idx); (cosmic_to_iced_weight(attrs.weight), cosmic_to_iced_style(attrs.style)) }; let push_span = |spans: &mut Vec>, range: std::ops::Range, key: StyleKey| { if range.start >= range.end || range.end > text.len() { return; } let mut span = Span::new(&text[range]); if let Some(raw) = key.0 { let c = cosmic_text::Color(raw); span = span.color(cosmic_to_iced(c, font_size_px)); } if key.1 != IcedWeight::Normal || key.2 != IcedStyle::Normal { span = span.font(Font { weight: key.1, style: key.2, ..base_font }); } spans.push(span); }; // subtract every marker range from the full line range. let visible_segments: Vec> = if marker_ranges.is_empty() { vec![0..text.len()] } else { let mut segs = vec![0..text.len()]; for marker in marker_ranges { let mut next = Vec::with_capacity(segs.len() + 1); for seg in segs.into_iter() { if marker.end <= seg.start || marker.start >= seg.end { next.push(seg); } else { if seg.start < marker.start { next.push(seg.start..marker.start); } if marker.end < seg.end { next.push(marker.end..seg.end); } } } segs = next; } segs }; if glyphs.is_empty() { // empty line — fall back to line-level attrs. let (w, s) = style_at(0); let key: StyleKey = (None, w, s); let mut spans = Vec::new(); for seg in &visible_segments { push_span(&mut spans, seg.clone(), key); } if spans.is_empty() { spans.push(Span::new(text)); } return spans; } let mut spans: Vec> = Vec::new(); for seg in &visible_segments { let mut seg_start = seg.start; let mut cur_key: Option = None; for glyph in glyphs { if glyph.start < seg.start || glyph.start >= seg.end { continue; } let (w, s) = style_at(glyph.start); let key: StyleKey = (glyph.color_opt.map(|c| c.0), w, s); match cur_key { None => { seg_start = glyph.start; cur_key = Some(key); } Some(prev) if prev != key => { push_span(&mut spans, seg_start..glyph.start, prev); seg_start = glyph.start; cur_key = Some(key); } _ => {} } } if let Some(key) = cur_key { push_span(&mut spans, seg_start..seg.end, key); } } if spans.is_empty() { spans.push(Span::new(text)); } spans } /// A multi-line text input. /// /// # Example /// ```no_run /// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # /// use iced::widget::text_editor; /// /// struct State { /// content: text_editor::Content, /// } /// /// #[derive(Debug, Clone)] /// enum Message { /// Edit(text_editor::Action) /// } /// /// fn view(state: &State) -> Element<'_, Message> { /// text_editor(&state.content) /// .placeholder("Type something here...") /// .on_action(Message::Edit) /// .into() /// } /// /// fn update(state: &mut State, message: Message) { /// match message { /// Message::Edit(action) => { /// state.content.perform(action); /// } /// } /// } /// ``` pub struct TextEditor< 'a, Highlighter, Message, Theme = iced_wgpu::core::Theme, > where Highlighter: text::Highlighter, Theme: Catalog, { id: Option, content: &'a Content, placeholder: Option>, font: Option, text_size: Option, line_height: LineHeight, width: Length, height: Length, min_height: f32, max_height: f32, padding: Padding, wrapping: Wrapping, class: Theme::Class<'a>, key_binding: Option Option> + 'a>>, on_edit: Option Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( &Highlighter::Highlight, &Theme, ) -> highlighter::Format, last_status: Option, // Acord extensions anchored_children: Vec>, gutter_offset: usize, is_focused_block: bool, show_gutter: bool, cursor_line: Option, line_indicator: crate::editor::LineIndicator, gutter_rainbow: bool, line_decors: Vec, } impl<'a, Message, Theme> TextEditor<'a, highlighter::PlainText, Message, Theme> where Theme: Catalog, { /// Creates new [`TextEditor`] with the given [`Content`]. pub fn new(content: &'a Content) -> Self { Self { id: None, content, placeholder: None, font: None, text_size: None, line_height: LineHeight::default(), width: Length::Fill, height: Length::Shrink, min_height: 0.0, max_height: f32::INFINITY, padding: Padding::new(5.0), wrapping: Wrapping::default(), class: ::default(), key_binding: None, on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { highlighter::Format::default() }, last_status: None, anchored_children: Vec::new(), gutter_offset: 0, is_focused_block: false, show_gutter: false, cursor_line: None, line_indicator: crate::editor::LineIndicator::On, gutter_rainbow: false, line_decors: Vec::new(), } } /// Sets the [`Id`](widget::Id) of the [`TextEditor`]. pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self } } impl<'a, Highlighter, Message, Theme> TextEditor<'a, Highlighter, Message, Theme> where Highlighter: text::Highlighter, Theme: Catalog, { /// Sets the placeholder of the [`TextEditor`]. pub fn placeholder( mut self, placeholder: impl text::IntoFragment<'a>, ) -> Self { self.placeholder = Some(placeholder.into_fragment()); self } /// Sets the width of the [`TextEditor`]. pub fn width(mut self, width: impl Into) -> Self { self.width = Length::from(width.into()); self } /// Sets the height of the [`TextEditor`]. pub fn height(mut self, height: impl Into) -> Self { self.height = height.into(); self } /// Sets the minimum height of the [`TextEditor`]. pub fn min_height(mut self, min_height: impl Into) -> Self { self.min_height = min_height.into().0; self } /// Sets the maximum height of the [`TextEditor`]. pub fn max_height(mut self, max_height: impl Into) -> Self { self.max_height = max_height.into().0; self } /// Sets the message that should be produced when some action is performed in /// the [`TextEditor`]. /// /// If this method is not called, the [`TextEditor`] will be disabled. pub fn on_action( mut self, on_edit: impl Fn(Action) -> Message + 'a, ) -> Self { self.on_edit = Some(Box::new(on_edit)); self } /// Sets the [`Font`] of the [`TextEditor`]. /// pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); self } /// Sets the text size of the [`TextEditor`]. pub fn size(mut self, size: impl Into) -> Self { self.text_size = Some(size.into()); self } /// Sets the [`text::LineHeight`] of the [`TextEditor`]. pub fn line_height( mut self, line_height: impl Into, ) -> Self { self.line_height = line_height.into(); self } /// Sets the [`Padding`] of the [`TextEditor`]. pub fn padding(mut self, padding: impl Into) -> Self { self.padding = padding.into(); self } /// Sets the [`Wrapping`] strategy of the [`TextEditor`]. pub fn wrapping(mut self, wrapping: Wrapping) -> Self { self.wrapping = wrapping; self } /// Highlights the [`TextEditor`] with the given [`Highlighter`] and /// a strategy to turn its highlights into some text format. pub fn highlight_with( self, settings: H::Settings, to_format: fn( &H::Highlight, &Theme, ) -> highlighter::Format, ) -> TextEditor<'a, H, Message, Theme> { TextEditor { id: self.id, content: self.content, placeholder: self.placeholder, font: self.font, text_size: self.text_size, line_height: self.line_height, width: self.width, height: self.height, min_height: self.min_height, max_height: self.max_height, padding: self.padding, wrapping: self.wrapping, class: self.class, key_binding: self.key_binding, on_edit: self.on_edit, highlighter_settings: settings, highlighter_format: to_format, last_status: self.last_status, anchored_children: self.anchored_children, gutter_offset: self.gutter_offset, is_focused_block: self.is_focused_block, show_gutter: self.show_gutter, cursor_line: self.cursor_line, line_indicator: self.line_indicator, gutter_rainbow: self.gutter_rainbow, line_decors: self.line_decors, } } /// Sets the closure to produce key bindings on key presses. /// /// See [`Binding`] for the list of available bindings. pub fn key_binding( mut self, key_binding: impl Fn(KeyPress) -> Option> + 'a, ) -> Self { self.key_binding = Some(Box::new(key_binding)); self } /// Sets the style of the [`TextEditor`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self where Theme::Class<'a>: From>, { self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); self } /// Sets the style class of the [`TextEditor`]. #[must_use] pub fn class(mut self, class: impl Into>) -> Self { self.class = class.into(); self } /// Sets the anchored child elements to draw at line boundaries. /// Items must be sorted by after_line. pub fn anchored(mut self, items: Vec>) -> Self { self.anchored_children = items; self } /// Sets the global line offset for gutter numbering. pub fn gutter_offset(mut self, offset: usize) -> Self { self.gutter_offset = offset; self } /// Marks this widget as the focused editing block. pub fn focused(mut self, focused: bool) -> Self { self.is_focused_block = focused; self } /// Reserve a left strip for line numbers + decoration stripes. pub fn show_gutter(mut self, show: bool) -> Self { self.show_gutter = show; self } /// Cursor's current line within this block. `None` when not focused. /// Drives both the cursorline tint and the gutter rainbow center. pub fn cursor_line(mut self, line: Option) -> Self { self.cursor_line = line; self } pub fn line_indicator(mut self, ind: crate::editor::LineIndicator) -> Self { self.line_indicator = ind; self } pub fn gutter_rainbow(mut self, on: bool) -> Self { self.gutter_rainbow = on; self } pub fn line_decors(mut self, decors: Vec) -> Self { self.line_decors = decors; self } /// Width of the gutter strip given a line count. Caller passes the /// count so this never touches `self.content` (which would deadlock /// when called from inside layout/draw — those already hold the /// content's RefCell). fn gutter_width_for(&self, line_count: usize) -> f32 { if !self.show_gutter { return 0.0; } let total = self.gutter_offset + line_count; let count = if total == 0 { 1 } else { total }; let digits = (count as f32).log10().floor() as usize + 1; let font_size: f32 = self.text_size .map(f32::from) .unwrap_or(14.0); let char_width = font_size * 0.6; (digits.max(2) as f32 * char_width + 16.0).ceil() } fn draw_gutter_line( &self, renderer: &mut iced_wgpu::Renderer, line_i: usize, bounds: Rectangle, y: f32, line_h: f32, gw: f32, p: &crate::palette::Palette, font_size: f32, ) { use crate::syntax::LineDecor; use crate::editor::LineIndicator; let gutter_left = bounds.x + self.padding.left; let gutter_right = gutter_left + gw; let decor = self.line_decors.get(line_i).copied().unwrap_or(LineDecor::None); match decor { LineDecor::CodeBlock | LineDecor::FenceMarker => { let bg = Color { a: 0.15, ..p.surface2 }; renderer.fill_quad( renderer::Quad { bounds: Rectangle::new( Point::new(gutter_left, y), Size::new(gw, line_h), ), border: Border::default(), ..renderer::Quad::default() }, Background::Color(bg), ); } LineDecor::Blockquote => { renderer.fill_quad( renderer::Quad { bounds: Rectangle::new( Point::new(gutter_right - 3.0, y), Size::new(3.0, line_h), ), border: Border::default(), ..renderer::Quad::default() }, Background::Color(p.lavender), ); } LineDecor::HorizontalRule => { let mid_y = y + line_h / 2.0; let stroke_color = crate::oklab::lighten_for_size(p.overlay1, 1.0); renderer.fill_quad( renderer::Quad { bounds: Rectangle::new( Point::new(gutter_left + 4.0, mid_y - 0.5), Size::new(gw - 8.0, 1.0), ), border: Border::default(), ..renderer::Quad::default() }, Background::Color(stroke_color), ); } LineDecor::None => {} } if self.line_indicator == LineIndicator::Off { return; } let raw_color = if self.gutter_rainbow { match self.cursor_line { Some(cl) if line_i == cl => p.text, Some(cl) if line_i > cl => { let d = line_i - cl - 1; let hue = crate::syntax::rainbow_color(d as u32); crate::oklab::desaturate(hue, gutter_fade_t(d)) } Some(cl) => { let d = cl - line_i - 1; let hue = crate::oklab::invert_hue(crate::syntax::rainbow_color(d as u32)); crate::oklab::desaturate(hue, gutter_fade_t(d)) } None => p.surface2, } } else { match self.cursor_line { Some(cl) if line_i == cl => p.text, _ => p.surface2, } }; let line_num = self.gutter_offset + line_i; let label = match (self.line_indicator, self.cursor_line) { (LineIndicator::Vim, Some(cl)) if line_i != cl => { let d = if line_i > cl { line_i - cl } else { cl - line_i }; format!("{d}") } _ => format!("{}", line_num + 1), }; renderer.fill_text( Text { content: label, bounds: Size::new(gw, line_h), size: Pixels(font_size), line_height: self.line_height, font: Font::MONOSPACE, align_x: text::Alignment::Right, align_y: alignment::Vertical::Top, shaping: text::Shaping::Basic, wrapping: Wrapping::None, }, Point::new(gutter_right - 8.0, y), crate::oklab::lighten_for_size(raw_color, font_size), Rectangle::new( Point::new(gutter_left, y), Size::new(gw, line_h), ), ); } fn input_method<'b>( &self, state: &'b State, renderer: &iced_wgpu::Renderer, layout: Layout<'_>, ) -> InputMethod<&'b str> { let Some(Focus { is_window_focused: true, .. }) = &state.focus else { return InputMethod::Disabled; }; let bounds = layout.bounds(); let internal = self.content.0.borrow_mut(); let gw = state.gutter_width.get(); let effective_padding = Padding { left: self.padding.left + gw, ..self.padding }; let text_bounds = bounds.shrink(effective_padding); let translation = text_bounds.position() - Point::ORIGIN; let cursor = match internal.editor.selection() { Selection::Caret(position) => position, Selection::Range(ranges) => { ranges.first().cloned().unwrap_or_default().position() } }; let line_height = self.line_height.to_absolute( self.text_size.unwrap_or_else(|| renderer.default_size()), ); let adjusted = { let metrics = state.line_metrics.borrow(); Point::new(cursor.x, cosmic_y_to_widget_y(&metrics, cursor.y, line_height.into())) }; let position = adjusted + translation; InputMethod::Enabled { cursor: Rectangle::new( position, Size::new(1.0, f32::from(line_height)), ), purpose: input_method::Purpose::Normal, preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), } } } /// The content of a [`TextEditor`]. pub struct Content(RefCell); struct Internal { editor: iced_graphics::text::Editor, } impl Content { /// Creates an empty [`Content`]. pub fn new() -> Self { Self::with_text("") } /// Creates a [`Content`] with the given text. pub fn with_text(text: &str) -> Self { Self(RefCell::new(Internal { editor: ::with_text(text), })) } /// Performs an [`Action`] on the [`Content`]. pub fn perform(&mut self, action: Action) { let internal = self.0.get_mut(); internal.editor.perform(action); } /// Moves the current cursor to reflect the given one. pub fn move_to(&mut self, cursor: Cursor) { let internal = self.0.get_mut(); internal.editor.move_to(cursor); } /// Returns the current cursor position of the [`Content`]. pub fn cursor(&self) -> Cursor { self.0.borrow().editor.cursor() } /// Returns the amount of lines of the [`Content`]. pub fn line_count(&self) -> usize { self.0.borrow().editor.line_count() } /// Returns the text of the line at the given index, if it exists. pub fn line(&self, index: usize) -> Option> { let internal = self.0.borrow(); let line = internal.editor.line(index)?; Some(Line { text: Cow::Owned(line.text.into_owned()), ending: line.ending, }) } /// Returns an iterator of the text of the lines in the [`Content`]. pub fn lines(&self) -> impl Iterator> { (0..) .map(|i| self.line(i)) .take_while(Option::is_some) .flatten() } /// Returns the text of the [`Content`]. pub fn text(&self) -> String { let mut contents = String::new(); let mut lines = self.lines().peekable(); while let Some(line) = lines.next() { contents.push_str(&line.text); if lines.peek().is_some() { contents.push_str(if line.ending == LineEnding::None { LineEnding::default().as_str() } else { line.ending.as_str() }); } } contents } /// Returns the selected text of the [`Content`]. pub fn selection(&self) -> Option { self.0.borrow().editor.copy() } /// Returns the kind of [`LineEnding`] used for separating lines in the [`Content`]. pub fn line_ending(&self) -> Option { Some(self.line(0)?.ending) } /// Returns whether or not the the [`Content`] is empty. pub fn is_empty(&self) -> bool { self.0.borrow().editor.is_empty() } } impl Clone for Content { fn clone(&self) -> Self { Self::with_text(&self.text()) } } impl Default for Content { fn default() -> Self { Self::new() } } impl fmt::Debug for Content { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let internal = self.0.borrow(); f.debug_struct("Content") .field("editor", &internal.editor) .finish() } } /// The state of a [`TextEditor`]. #[derive(Debug)] pub struct State { focus: Option, preedit: Option, last_click: Option, drag_click: Option, partial_scroll: f32, last_theme: RefCell>, highlighter: RefCell, highlighter_settings: Highlighter::Settings, highlighter_format_address: usize, /// Paragraphs built during draw() — kept alive so the renderer's Weak refs /// survive until the prepare() phase processes them. retained_paragraphs: RefCell>, /// Per-logical-line metrics published by `layout()`. Every consumer /// (draw, cursor caret, click/drag hit-testing, IME) reads from this /// same Vec — there is no parallel computation. Length = line_count /// + 1, with the trailing sentinel marking widget/buffer y past the /// last line. line_metrics: RefCell>, /// Gutter strip width, also published by layout. Same single-source /// rule: events translate click x by reading this, never recomputing. gutter_width: std::cell::Cell, } #[derive(Debug, Clone)] struct Focus { updated_at: Instant, now: Instant, is_window_focused: bool, } impl Focus { const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; fn now() -> Self { let now = Instant::now(); Self { updated_at: now, now, is_window_focused: true, } } fn is_cursor_visible(&self) -> bool { self.is_window_focused && ((self.now - self.updated_at).as_millis() / Self::CURSOR_BLINK_INTERVAL_MILLIS) .is_multiple_of(2) } } impl State { /// Returns whether the [`TextEditor`] is currently focused or not. pub fn is_focused(&self) -> bool { self.focus.is_some() } } impl operation::Focusable for State { fn is_focused(&self) -> bool { self.focus.is_some() } fn focus(&mut self) { self.focus = Some(Focus::now()); } fn unfocus(&mut self) { self.focus = None; } } impl Widget for TextEditor<'_, Highlighter, Message, Theme> where Highlighter: text::Highlighter, Theme: Catalog, { fn tag(&self) -> widget::tree::Tag { widget::tree::Tag::of::>() } fn state(&self) -> widget::tree::State { widget::tree::State::new(State { focus: None, preedit: None, last_click: None, drag_click: None, partial_scroll: 0.0, last_theme: RefCell::default(), highlighter: RefCell::new(Highlighter::new( &self.highlighter_settings, )), highlighter_settings: self.highlighter_settings.clone(), highlighter_format_address: self.highlighter_format as usize, retained_paragraphs: RefCell::new(Vec::new()), line_metrics: RefCell::new(Vec::new()), gutter_width: std::cell::Cell::new(0.0), }) } fn children(&self) -> Vec { self.anchored_children.iter().map(|item| widget::Tree::new(&item.element)).collect() } fn diff(&self, tree: &mut widget::Tree) { tree.diff_children(&self.anchored_children.iter().map(|item| &item.element).collect::>()); } fn size(&self) -> Size { Size { width: self.width, height: self.height, } } fn layout( &mut self, tree: &mut widget::Tree, renderer: &iced_wgpu::Renderer, limits: &layout::Limits, ) -> layout::Node { let mut internal = self.content.0.borrow_mut(); let state = tree.state.downcast_mut::>(); if state.highlighter_format_address != self.highlighter_format as usize { state.highlighter.borrow_mut().change_line(0); state.highlighter_format_address = self.highlighter_format as usize; } if state.highlighter_settings != self.highlighter_settings { state .highlighter .borrow_mut() .update(&self.highlighter_settings); state.highlighter_settings = self.highlighter_settings.clone(); } let limits = limits .width(self.width) .height(self.height) .min_height(self.min_height) .max_height(self.max_height); let gw = self.gutter_width_for(internal.editor.line_count()); state.gutter_width.set(gw); let effective_padding = Padding { left: self.padding.left + gw, ..self.padding }; internal.editor.update( limits.shrink(effective_padding).max(), self.font.unwrap_or_else(|| renderer.default_font()), self.text_size.unwrap_or_else(|| renderer.default_size()), self.line_height, self.wrapping, state.highlighter.borrow_mut().deref_mut(), ); let line_h: f32 = self.line_height.to_absolute( self.text_size.unwrap_or_else(|| renderer.default_size()), ).into(); // Single source-of-truth: walk lines + anchored children once and // build per-line metrics. Each LineMetric records the widget-y + // buffer-y of that line's first visual row, plus the wrap count. // Draw, cursor positioning, click/drag — every consumer reads from // this same Vec so they cannot drift. let mut child_nodes = Vec::with_capacity(self.anchored_children.len()); let child_limits = layout::Limits::new( Size::ZERO, Size::new(limits.shrink(effective_padding).max().width, f32::INFINITY), ); let buffer = internal.editor.buffer(); let line_count = buffer.lines.len(); // Seed widget_y from cosmic-text's internal scroll so the metrics // we publish reflect ACTUAL on-screen positions, not no-scroll // positions. Without this seeding, draw renders text at unscrolled // y while the cursor (computed via cosmic's scroll-aware selection) // appears to drift — the classic "two sources of truth" violation. // Anchor cosmic's viewport-y at scroll.line top: cosmic's // `Selection::Caret(position).y` for a cursor sitting on the very // first visible visual row equals `-scroll.vertical`, regardless // of how many logical lines came before. Pre-scroll lines are not // shaped (`layout_opt() == None`) and contribute 0 visual rows in // cosmic's own bookkeeping — mirror that so the two y-spaces // agree. let scroll = buffer.scroll(); let mut metrics: Vec = Vec::with_capacity(line_count + 1); let mut widget_y = -scroll.vertical; let mut viewport_y = -scroll.vertical; // Pre-scroll lines: cosmic treats them as 0 rows, but we still // need a widget-y so any bottom-anchored items render relative to // a stable bottom. For the cursor/selection mapping to work the // viewport_y must stay parked at -scroll.vertical for them // (cosmic has no addressable y above scroll.line). for _ in 0..scroll.line.min(line_count) { metrics.push(LineMetric { widget_y, viewport_y, visual_rows: 0 }); } let mut next_child = 0; // Skip anchored children that sit above the scroll line. while next_child < self.anchored_children.len() && self.anchored_children[next_child].after_line < scroll.line { next_child += 1; } for line in scroll.line..line_count { let visual_rows = buffer.lines[line] .layout_opt() .map(|v| v.len()) .unwrap_or(1) .max(1); metrics.push(LineMetric { widget_y, viewport_y, visual_rows }); let line_visual_h = visual_rows as f32 * line_h; widget_y += line_visual_h; viewport_y += line_visual_h; while next_child < self.anchored_children.len() && self.anchored_children[next_child].after_line == line { let child = &mut self.anchored_children[next_child]; let mut node = child.element.as_widget_mut().layout( &mut tree.children[next_child], renderer, &child_limits, ); node = node.move_to(Point::new( self.padding.left + gw, self.padding.top + widget_y, )); child.height = node.bounds().height; widget_y += child.height; child_nodes.push(node); next_child += 1; } } // Remaining children after last line — they sit below all text. while next_child < self.anchored_children.len() { let child = &mut self.anchored_children[next_child]; let mut node = child.element.as_widget_mut().layout( &mut tree.children[next_child], renderer, &child_limits, ); node = node.move_to(Point::new( self.padding.left + gw, self.padding.top + widget_y, )); child.height = node.bounds().height; widget_y += child.height; child_nodes.push(node); next_child += 1; } // Push sentinel AFTER trailing children are placed, so the // sentinel widget_y reflects the true bottom of the stream. metrics.push(LineMetric { widget_y, viewport_y, visual_rows: 0 }); let extra = widget_y - viewport_y; *state.line_metrics.borrow_mut() = metrics; match self.height { Length::Fill | Length::FillPortion(_) | Length::Fixed(_) => { // Fixed/Fill: caller specified the height. Honor it as-is — // anchored items live within that height; trailing space // would otherwise create phantom gaps below the block. layout::Node::with_children(limits.max(), child_nodes) } Length::Shrink => { let min_bounds = internal.editor.min_bounds(); layout::Node::with_children( limits .height(min_bounds.height + extra) .max() .expand(Size::new(0.0, self.padding.y())), child_nodes, ) } } } fn update( &mut self, tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &iced_wgpu::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) { // Forward events to anchored children first if !self.anchored_children.is_empty() { let children_layouts: Vec<_> = layout.children().collect(); for (i, child) in self.anchored_children.iter_mut().enumerate() { if i < children_layouts.len() && i < tree.children.len() { child.element.as_widget_mut().update( &mut tree.children[i], event, children_layouts[i], cursor, renderer, clipboard, shell, _viewport, ); if shell.is_event_captured() { return; } } } } let Some(on_edit) = self.on_edit.as_ref() else { return; }; let state = tree.state.downcast_mut::>(); let is_redraw = matches!( event, Event::Window(window::Event::RedrawRequested(_now)), ); match event { Event::Window(window::Event::Unfocused) => { if let Some(focus) = &mut state.focus { focus.is_window_focused = false; } } Event::Window(window::Event::Focused) => { if let Some(focus) = &mut state.focus { focus.is_window_focused = true; focus.updated_at = Instant::now(); shell.request_redraw(); } } Event::Window(window::Event::RedrawRequested(now)) => { if let Some(focus) = &mut state.focus && focus.is_window_focused { focus.now = *now; let millis_until_redraw = Focus::CURSOR_BLINK_INTERVAL_MILLIS - (focus.now - focus.updated_at).as_millis() % Focus::CURSOR_BLINK_INTERVAL_MILLIS; shell.request_redraw_at( focus.now + Duration::from_millis(millis_until_redraw as u64), ); } } _ => {} } if let Some(update) = Update::from_event( event, state, layout.bounds(), self.padding, cursor, self.key_binding.as_deref(), ) { let line_h: f32 = self.line_height.to_absolute( self.text_size.unwrap_or_else(|| renderer.default_size()), ).into(); match update { Update::Click(click) => { let gw = state.gutter_width.get(); let action = match click.kind() { mouse::click::Kind::Single => { let mut pos = click.position(); pos.x = (pos.x - gw).max(0.0); let metrics = state.line_metrics.borrow(); pos.y = widget_y_to_cosmic_y(&metrics, pos.y, line_h); Action::Click(pos) } mouse::click::Kind::Double => Action::SelectWord, mouse::click::Kind::Triple => Action::SelectLine, }; state.focus = Some(Focus::now()); state.last_click = Some(click); state.drag_click = Some(click.kind()); shell.publish(on_edit(action)); shell.capture_event(); } Update::Drag(position) => { let gw = state.gutter_width.get(); let mut pos = position; pos.x = (pos.x - gw).max(0.0); let metrics = state.line_metrics.borrow(); pos.y = widget_y_to_cosmic_y(&metrics, pos.y, line_h); shell.publish(on_edit(Action::Drag(pos))); } Update::Release => { state.drag_click = None; } Update::Scroll(lines) => { let bounds = self.content.0.borrow().editor.bounds(); if bounds.height >= i32::MAX as f32 { return; } let lines = lines + state.partial_scroll; state.partial_scroll = lines.fract(); shell.publish(on_edit(Action::Scroll { lines: lines as i32, })); shell.capture_event(); } Update::InputMethod(update) => match update { Ime::Toggle(is_open) => { state.preedit = is_open.then(input_method::Preedit::new); shell.request_redraw(); } Ime::Preedit { content, selection } => { state.preedit = Some(input_method::Preedit { content, selection, text_size: self.text_size, }); shell.request_redraw(); } Ime::Commit(text) => { shell.publish(on_edit(Action::Edit(Edit::Paste( Arc::new(text), )))); } }, Update::Binding(binding) => { fn apply_binding< H: text::Highlighter, Message, >( binding: Binding, content: &Content, state: &mut State, on_edit: &dyn Fn(Action) -> Message, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) { let mut publish = |action| shell.publish(on_edit(action)); match binding { Binding::Unfocus => { state.focus = None; state.drag_click = None; } Binding::Copy => { if let Some(selection) = content.selection() { clipboard.write( clipboard::Kind::Standard, selection, ); } } Binding::Cut => { if let Some(selection) = content.selection() { clipboard.write( clipboard::Kind::Standard, selection, ); publish(Action::Edit(Edit::Delete)); } } Binding::Paste => { if let Some(contents) = clipboard.read(clipboard::Kind::Standard) { publish(Action::Edit(Edit::Paste( Arc::new(contents), ))); } } Binding::Move(motion) => { publish(Action::Move(motion)); } Binding::Select(motion) => { publish(Action::Select(motion)); } Binding::SelectWord => { publish(Action::SelectWord); } Binding::SelectLine => { publish(Action::SelectLine); } Binding::SelectAll => { publish(Action::SelectAll); } Binding::Insert(c) => { publish(Action::Edit(Edit::Insert(c))); } Binding::Enter => { publish(Action::Edit(Edit::Enter)); } Binding::Backspace => { publish(Action::Edit(Edit::Backspace)); } Binding::Delete => { publish(Action::Edit(Edit::Delete)); } Binding::Sequence(sequence) => { for binding in sequence { apply_binding( binding, content, state, on_edit, clipboard, shell, ); } } Binding::Custom(message) => { shell.publish(message); } } } if !matches!(binding, Binding::Unfocus) { shell.capture_event(); } apply_binding( binding, self.content, state, on_edit, clipboard, shell, ); if let Some(focus) = &mut state.focus { focus.updated_at = Instant::now(); } } } } let status = { let is_disabled = self.on_edit.is_none(); let is_hovered = cursor.is_over(layout.bounds()); if is_disabled { Status::Disabled } else if state.focus.is_some() { Status::Focused { is_hovered } } else if is_hovered { Status::Hovered } else { Status::Active } }; if is_redraw { self.last_status = Some(status); shell.request_input_method( &self.input_method(state, renderer, layout), ); } else if self .last_status .is_some_and(|last_status| status != last_status) { shell.request_redraw(); } } fn draw( &self, tree: &widget::Tree, renderer: &mut iced_wgpu::Renderer, theme: &Theme, _defaults: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); let mut internal = self.content.0.borrow_mut(); let state = tree.state.downcast_ref::>(); let font = self.font.unwrap_or_else(|| renderer.default_font()); let theme_name = theme.name(); if state .last_theme .borrow() .as_ref() .is_none_or(|last_theme| last_theme != theme_name) { state.highlighter.borrow_mut().change_line(0); let _ = state.last_theme.borrow_mut().replace(theme_name.to_owned()); } internal.editor.highlight( font, state.highlighter.borrow_mut().deref_mut(), |highlight| (self.highlighter_format)(highlight, theme), ); let style = theme .style(&self.class, self.last_status.unwrap_or(Status::Active)); renderer.fill_quad( renderer::Quad { bounds, border: style.border, ..renderer::Quad::default() }, style.background, ); let gw = state.gutter_width.get(); let effective_padding = Padding { left: self.padding.left + gw, ..self.padding }; let text_bounds = bounds.shrink(effective_padding); let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); let line_h: f32 = self.line_height.to_absolute(text_size).into(); // Gutter background — only the strip below top_pad so the title-bar // / traffic-light area doesn't get painted. if self.show_gutter && self.padding.top < bounds.height { let p = crate::palette::current(); renderer.fill_quad( renderer::Quad { bounds: Rectangle::new( Point::new(bounds.x, bounds.y + self.padding.top), Size::new(gw + self.padding.left, bounds.height - self.padding.top), ), border: Border::default(), ..renderer::Quad::default() }, Background::Color(p.crust), ); } if internal.editor.is_empty() { if let Some(placeholder) = self.placeholder.clone() { renderer.fill_text( Text { content: placeholder.into_owned(), bounds: text_bounds.size(), size: text_size, line_height: self.line_height, font, align_x: text::Alignment::Default, align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: self.wrapping, }, text_bounds.position(), style.placeholder, text_bounds, ); } } else { // Sequential stream: text lines (layer 0) interleaved with // anchored children (layer 1) in one continuous pass. Cursorline // tint and gutter line numbers are drawn on the SAME y as the // line's paragraph — single source of truth. let buffer = internal.editor.buffer(); let line_count = buffer.lines.len(); let mut child_idx = 0; let children_layouts: Vec<_> = layout.children().collect(); // cursor line draws raw cosmic glyphs, other lines strip marker bytes for WYS. let active_cursor_line = if self.is_focused_block { self.cursor_line } else { None }; let highlighter_borrow = state.highlighter.borrow(); let highlighter_any: &dyn std::any::Any = &*highlighter_borrow; let syntax_highlighter = highlighter_any.downcast_ref::(); { let mut paras = state.retained_paragraphs.borrow_mut(); paras.clear(); let metrics = state.line_metrics.borrow(); for i in 0..line_count { let line_text = buffer.lines[i].text(); let attrs_list = buffer.lines[i].attrs_list(); let glyphs: Vec = buffer.lines[i].layout_opt() .map(|layouts| layouts.iter().flat_map(|l| l.glyphs.iter().cloned()).collect()) .unwrap_or_default(); let marker_ranges = if active_cursor_line == Some(i) { Vec::new() } else { syntax_highlighter .map(|h| h.line_marker_ranges(i, line_text)) .unwrap_or_default() }; let spans = build_color_spans( line_text, &glyphs, attrs_list, font, f32::from(text_size), &marker_ranges, ); let visual_rows = metrics.get(i).map(|m| m.visual_rows).unwrap_or(1).max(1); paras.push(iced_graphics::text::Paragraph::with_spans(Text { content: spans.as_slice(), bounds: Size::new(text_bounds.width, visual_rows as f32 * line_h), size: text_size, line_height: self.line_height, font, align_x: text::Alignment::Default, align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: self.wrapping, })); } } let p = crate::palette::current(); let font_size_px: f32 = f32::from(text_size); let paras = state.retained_paragraphs.borrow(); let metrics = state.line_metrics.borrow(); for line_i in 0..line_count { // Pull line position from the Vec layout published. let m = match metrics.get(line_i) { Some(m) => m, None => continue, }; // Pre-scroll lines carry visual_rows == 0 (cosmic hasn't // shaped them, layout_opt returns None) — skip them so // we don't draw unshaped paragraphs piled at the same y. if m.visual_rows == 0 { continue; } let y = text_bounds.y + m.widget_y; let row_h = m.visual_rows as f32 * line_h; // Cursorline tint — full editor width (incl. gutter), // covers all visual rows of the wrapped logical line. if self.is_focused_block && self.cursor_line == Some(line_i) && self.line_indicator != crate::editor::LineIndicator::Off { let band = Color { a: 0.06, ..p.text }; renderer.fill_quad( renderer::Quad { bounds: Rectangle::new( Point::new(bounds.x, y), Size::new(bounds.width, row_h), ), border: Border::default(), ..renderer::Quad::default() }, Background::Color(band), ); } // Gutter — line decor stripe + line number, in the strip // between bounds.x and text_bounds.x. if self.show_gutter { self.draw_gutter_line(renderer, line_i, bounds, y, line_h, gw, &p, font_size_px); } renderer.fill_paragraph( ¶s[line_i], Point::new(text_bounds.x, y), style.value, text_bounds, ); // After this line, draw any anchored children while child_idx < self.anchored_children.len() && self.anchored_children[child_idx].after_line == line_i { if child_idx < children_layouts.len() { self.anchored_children[child_idx].element.as_widget().draw( &tree.children[child_idx], renderer, theme, _defaults, children_layouts[child_idx], _cursor, _viewport, ); } child_idx += 1; } } // Draw remaining children after last text line while child_idx < self.anchored_children.len() { if child_idx < children_layouts.len() { self.anchored_children[child_idx].element.as_widget().draw( &tree.children[child_idx], renderer, theme, _defaults, children_layouts[child_idx], _cursor, _viewport, ); } child_idx += 1; } } let translation = text_bounds.position() - Point::ORIGIN; if let Some(focus) = state.focus.as_ref() { let metrics_for_cursor = state.line_metrics.borrow(); let adjust_y = |pos: Point| -> Point { Point::new(pos.x, cosmic_y_to_widget_y(&metrics_for_cursor, pos.y, line_h)) }; match internal.editor.selection() { Selection::Caret(position) if focus.is_cursor_visible() => { let position = adjust_y(position); let cursor = Rectangle::new( position + translation, Size::new(1.0, line_h), ); if let Some(clipped_cursor) = text_bounds.intersection(&cursor) { renderer.fill_quad( renderer::Quad { bounds: clipped_cursor, ..renderer::Quad::default() }, style.value, ); } } Selection::Range(ranges) => { for range in ranges.into_iter().map(|r| { let adjusted = Rectangle::new( adjust_y(r.position()), r.size(), ); adjusted + translation }).filter_map(|r| text_bounds.intersection(&r)) { renderer.fill_quad( renderer::Quad { bounds: range, ..renderer::Quad::default() }, style.selection, ); } } Selection::Caret(_) => {} } } } fn mouse_interaction( &self, _tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &iced_wgpu::Renderer, ) -> mouse::Interaction { let is_disabled = self.on_edit.is_none(); if cursor.is_over(layout.bounds()) { if is_disabled { mouse::Interaction::NotAllowed } else { mouse::Interaction::Text } } else { mouse::Interaction::default() } } fn operate( &mut self, tree: &mut widget::Tree, layout: Layout<'_>, _renderer: &iced_wgpu::Renderer, operation: &mut dyn widget::Operation, ) { let state = tree.state.downcast_mut::>(); operation.focusable(self.id.as_ref(), layout.bounds(), state); } } impl<'a, Highlighter, Message, Theme> From> for Element<'a, Message, Theme, iced_wgpu::Renderer> where Highlighter: text::Highlighter, Message: 'a, Theme: Catalog + 'a, { fn from( text_editor: TextEditor<'a, Highlighter, Message, Theme>, ) -> Self { Self::new(text_editor) } } /// A binding to an action in the [`TextEditor`]. #[derive(Debug, Clone, PartialEq)] pub enum Binding { /// Unfocus the [`TextEditor`]. Unfocus, /// Copy the selection of the [`TextEditor`]. Copy, /// Cut the selection of the [`TextEditor`]. Cut, /// Paste the clipboard contents in the [`TextEditor`]. Paste, /// Apply a [`Motion`]. Move(Motion), /// Select text with a given [`Motion`]. Select(Motion), /// Select the word at the current cursor. SelectWord, /// Select the line at the current cursor. SelectLine, /// Select the entire buffer. SelectAll, /// Insert the given character. Insert(char), /// Break the current line. Enter, /// Delete the previous character. Backspace, /// Delete the next character. Delete, /// A sequence of bindings to execute. Sequence(Vec), /// Produce the given message. Custom(Message), } /// A key press. #[derive(Debug, Clone, PartialEq, Eq)] pub struct KeyPress { /// The original key pressed without modifiers applied to it. /// /// You should use this key for combinations (e.g. Ctrl+C). pub key: keyboard::Key, /// The key pressed with modifiers applied to it. /// /// You should use this key for any single key bindings (e.g. motions). pub modified_key: keyboard::Key, /// The physical key pressed. /// /// You should use this key for layout-independent bindings. pub physical_key: keyboard::key::Physical, /// The state of the keyboard modifiers. pub modifiers: keyboard::Modifiers, /// The text produced by the key press. pub text: Option, /// The current [`Status`] of the [`TextEditor`]. pub status: Status, } impl Binding { /// Returns the default [`Binding`] for the given key press. pub fn from_key_press(event: KeyPress) -> Option { let KeyPress { key, modified_key, physical_key, modifiers, text, status, } = event; if !matches!(status, Status::Focused { .. }) { return None; } let combination = match key.to_latin(physical_key) { Some('c') if modifiers.command() => Some(Self::Copy), Some('x') if modifiers.command() => Some(Self::Cut), Some('v') if modifiers.command() && !modifiers.alt() => { Some(Self::Paste) } Some('a') if modifiers.command() => Some(Self::SelectAll), _ => None, }; if let Some(binding) = combination { return Some(binding); } #[cfg(target_os = "macos")] let modified_key = convert_macos_shortcut(&key, modifiers).unwrap_or(modified_key); match modified_key.as_ref() { keyboard::Key::Named(key::Named::Enter) => Some(Self::Enter), keyboard::Key::Named(key::Named::Backspace) => { Some(Self::Backspace) } keyboard::Key::Named(key::Named::Delete) if text.is_none() || text.as_deref() == Some("\u{7f}") => { Some(Self::Delete) } keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus), _ => { if let Some(text) = text { let c = text.chars().find(|c| !c.is_control())?; Some(Self::Insert(c)) } else if let keyboard::Key::Named(named_key) = key.as_ref() { let motion = motion(named_key)?; let motion = if modifiers.macos_command() { match motion { Motion::Left => Motion::Home, Motion::Right => Motion::End, _ => motion, } } else { motion }; let motion = if modifiers.jump() { motion.widen() } else { motion }; Some(if modifiers.shift() { Self::Select(motion) } else { Self::Move(motion) }) } else { None } } } } } enum Update { Click(mouse::Click), Drag(Point), Release, Scroll(f32), InputMethod(Ime), Binding(Binding), } enum Ime { Toggle(bool), Preedit { content: String, selection: Option>, }, Commit(String), } impl Update { fn from_event( event: &Event, state: &State, bounds: Rectangle, padding: Padding, cursor: mouse::Cursor, key_binding: Option<&dyn Fn(KeyPress) -> Option>>, ) -> Option { let binding = |binding| Some(Update::Binding(binding)); match event { Event::Mouse(event) => match event { mouse::Event::ButtonPressed(mouse::Button::Left) => { if let Some(cursor_position) = cursor.position_in(bounds) { let cursor_position = cursor_position - Vector::new(padding.left, padding.top); let click = mouse::Click::new( cursor_position, mouse::Button::Left, state.last_click, ); Some(Update::Click(click)) } else if state.focus.is_some() { binding(Binding::Unfocus) } else { None } } mouse::Event::ButtonReleased(mouse::Button::Left) => { Some(Update::Release) } mouse::Event::CursorMoved { .. } => match state.drag_click { Some(mouse::click::Kind::Single) => { let cursor_position = cursor.position_in(bounds)? - Vector::new(padding.left, padding.top); Some(Update::Drag(cursor_position)) } _ => None, }, mouse::Event::WheelScrolled { delta } if cursor.is_over(bounds) => { Some(Update::Scroll(match delta { mouse::ScrollDelta::Lines { y, .. } => { if y.abs() > 0.0 { y.signum() * -(y.abs() * 4.0).max(1.0) } else { 0.0 } } mouse::ScrollDelta::Pixels { y, .. } => -y / 4.0, })) } _ => None, }, Event::InputMethod(event) => match event { input_method::Event::Opened | input_method::Event::Closed => { Some(Update::InputMethod(Ime::Toggle(matches!( event, input_method::Event::Opened )))) } input_method::Event::Preedit(content, selection) if state.focus.is_some() => { Some(Update::InputMethod(Ime::Preedit { content: content.clone(), selection: selection.clone(), })) } input_method::Event::Commit(content) if state.focus.is_some() => { Some(Update::InputMethod(Ime::Commit(content.clone()))) } _ => None, }, Event::Keyboard(keyboard::Event::KeyPressed { key, modified_key, physical_key, modifiers, text, .. }) => { let status = if state.focus.is_some() { Status::Focused { is_hovered: cursor.is_over(bounds), } } else { Status::Active }; let key_press = KeyPress { key: key.clone(), modified_key: modified_key.clone(), physical_key: *physical_key, modifiers: *modifiers, text: text.clone(), status, }; if let Some(key_binding) = key_binding { key_binding(key_press) } else { Binding::from_key_press(key_press) } .map(Self::Binding) } _ => None, } } } fn motion(key: key::Named) -> Option { match key { key::Named::ArrowLeft => Some(Motion::Left), key::Named::ArrowRight => Some(Motion::Right), key::Named::ArrowUp => Some(Motion::Up), key::Named::ArrowDown => Some(Motion::Down), key::Named::Home => Some(Motion::Home), key::Named::End => Some(Motion::End), key::Named::PageUp => Some(Motion::PageUp), key::Named::PageDown => Some(Motion::PageDown), _ => None, } } /// The possible status of a [`TextEditor`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { /// The [`TextEditor`] can be interacted with. Active, /// The [`TextEditor`] is being hovered. Hovered, /// The [`TextEditor`] is focused. Focused { /// Whether the [`TextEditor`] is hovered, while focused. is_hovered: bool, }, /// The [`TextEditor`] cannot be interacted with. Disabled, } /// The appearance of a text input. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the text input. pub background: Background, /// The [`Border`] of the text input. pub border: Border, /// The [`Color`] of the placeholder of the text input. pub placeholder: Color, /// The [`Color`] of the value of the text input. pub value: Color, /// The [`Color`] of the selection of the text input. pub selection: Color, } /// The theme catalog of a [`TextEditor`]. pub trait Catalog: theme::Base { /// The item class of the [`Catalog`]. type Class<'a>; /// The default class produced by the [`Catalog`]. fn default<'a>() -> Self::Class<'a>; /// The [`Style`] of a class with the given status. fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } /// A styling function for a [`TextEditor`]. pub type StyleFn<'a, Theme> = Box Style + 'a>; impl Catalog for Theme { type Class<'a> = StyleFn<'a, Self>; fn default<'a>() -> Self::Class<'a> { Box::new(default) } fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { class(self, status) } } /// The default style of a [`TextEditor`]. pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let active = Style { background: Background::Color(palette.background.base.color), border: Border { radius: 2.0.into(), width: 1.0, color: palette.background.strong.color, }, placeholder: palette.secondary.base.color, value: palette.background.base.text, selection: palette.primary.weak.color, }; match status { Status::Active => active, Status::Hovered => Style { border: Border { color: palette.background.base.text, ..active.border }, ..active }, Status::Focused { .. } => Style { border: Border { color: palette.primary.strong.color, ..active.border }, ..active }, Status::Disabled => Style { background: Background::Color(palette.background.weak.color), value: active.placeholder, placeholder: palette.background.strongest.color, ..active }, } } #[cfg(target_os = "macos")] pub fn convert_macos_shortcut( key: &keyboard::Key, modifiers: keyboard::Modifiers, ) -> Option { if modifiers != keyboard::Modifiers::CTRL { return None; } let key = match key.as_ref() { keyboard::Key::Character("b") => key::Named::ArrowLeft, keyboard::Key::Character("f") => key::Named::ArrowRight, keyboard::Key::Character("a") => key::Named::Home, keyboard::Key::Character("e") => key::Named::End, keyboard::Key::Character("h") => key::Named::Backspace, keyboard::Key::Character("d") => key::Named::Delete, _ => return None, }; Some(keyboard::Key::Named(key)) }