// SPDX-License-Identifier: MIT OR Apache-2.0 #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; use core::{cmp, fmt}; use unicode_segmentation::UnicodeSegmentation; use crate::{ Affinity, Align, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color, Cursor, FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, LineEnding, LineIter, Motion, Scroll, ShapeLine, Shaping, Wrap, }; /// A line of visible text for rendering #[derive(Debug)] pub struct LayoutRun<'a> { /// The index of the original text line pub line_i: usize, /// The original text line pub text: &'a str, /// True if the original paragraph direction is RTL pub rtl: bool, /// The array of layout glyphs to draw pub glyphs: &'a [LayoutGlyph], /// Y offset to baseline of line pub line_y: f32, /// Y offset to top of line pub line_top: f32, /// Y offset to next line pub line_height: f32, /// Width of line pub line_w: f32, } impl<'a> LayoutRun<'a> { /// Return the pixel span `Some((x_left, x_width))` of the highlighted area between `cursor_start` /// and `cursor_end` within this run, or None if the cursor range does not intersect this run. /// This may return widths of zero if `cursor_start == cursor_end`, if the run is empty, or if the /// region's left start boundary is the same as the cursor's end boundary or vice versa. pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> { let mut x_start = None; let mut x_end = None; let rtl_factor = if self.rtl { 1. } else { 0. }; let ltr_factor = 1. - rtl_factor; for glyph in self.glyphs.iter() { let cursor = self.cursor_from_glyph_left(glyph); if cursor >= cursor_start && cursor <= cursor_end { if x_start.is_none() { x_start = Some(glyph.x + glyph.w * rtl_factor); } x_end = Some(glyph.x + glyph.w * rtl_factor); } let cursor = self.cursor_from_glyph_right(glyph); if cursor >= cursor_start && cursor <= cursor_end { if x_start.is_none() { x_start = Some(glyph.x + glyph.w * ltr_factor); } x_end = Some(glyph.x + glyph.w * ltr_factor); } } if let Some(x_start) = x_start { let x_end = x_end.expect("end of cursor not found"); let (x_start, x_end) = if x_start < x_end { (x_start, x_end) } else { (x_end, x_start) }; Some((x_start, x_end - x_start)) } else { None } } fn cursor_from_glyph_left(&self, glyph: &LayoutGlyph) -> Cursor { if self.rtl { Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before) } else { Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After) } } fn cursor_from_glyph_right(&self, glyph: &LayoutGlyph) -> Cursor { if self.rtl { Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After) } else { Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before) } } } /// An iterator of visible text lines, see [`LayoutRun`] #[derive(Debug)] pub struct LayoutRunIter<'b> { buffer: &'b Buffer, line_i: usize, layout_i: usize, total_height: f32, line_top: f32, } impl<'b> LayoutRunIter<'b> { pub fn new(buffer: &'b Buffer) -> Self { Self { buffer, line_i: buffer.scroll.line, layout_i: 0, total_height: 0.0, line_top: 0.0, } } } impl<'b> Iterator for LayoutRunIter<'b> { type Item = LayoutRun<'b>; fn next(&mut self) -> Option { while let Some(line) = self.buffer.lines.get(self.line_i) { let shape = line.shape_opt()?; let layout = line.layout_opt()?; while let Some(layout_line) = layout.get(self.layout_i) { self.layout_i += 1; let line_height = layout_line .line_height_opt .unwrap_or(self.buffer.metrics.line_height); self.total_height += line_height; let line_top = self.line_top - self.buffer.scroll.vertical; let glyph_height = layout_line.max_ascent + layout_line.max_descent; let centering_offset = (line_height - glyph_height) / 2.0; let line_y = line_top + centering_offset + layout_line.max_ascent; if let Some(height) = self.buffer.height_opt { if line_y > height { return None; } } self.line_top += line_height; if line_y < 0.0 { continue; } return Some(LayoutRun { line_i: self.line_i, text: line.text(), rtl: shape.rtl, glyphs: &layout_line.glyphs, line_y, line_top, line_height, line_w: layout_line.w, }); } self.line_i += 1; self.layout_i = 0; } None } } /// Metrics of text #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Metrics { /// Font size in pixels pub font_size: f32, /// Line height in pixels pub line_height: f32, } impl Metrics { /// Create metrics with given font size and line height pub const fn new(font_size: f32, line_height: f32) -> Self { Self { font_size, line_height, } } /// Create metrics with given font size and calculate line height using relative scale pub fn relative(font_size: f32, line_height_scale: f32) -> Self { Self { font_size, line_height: font_size * line_height_scale, } } /// Scale font size and line height pub fn scale(self, scale: f32) -> Self { Self { font_size: self.font_size * scale, line_height: self.line_height * scale, } } } impl fmt::Display for Metrics { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}px / {}px", self.font_size, self.line_height) } } /// A buffer of text that is shaped and laid out #[derive(Debug)] pub struct Buffer { /// [BufferLine]s (or paragraphs) of text in the buffer pub lines: Vec, metrics: Metrics, width_opt: Option, height_opt: Option, scroll: Scroll, /// True if a redraw is requires. Set to false after processing redraw: bool, wrap: Wrap, monospace_width: Option, tab_width: u16, } impl Clone for Buffer { fn clone(&self) -> Self { Self { lines: self.lines.clone(), metrics: self.metrics, width_opt: self.width_opt, height_opt: self.height_opt, scroll: self.scroll, redraw: self.redraw, wrap: self.wrap, monospace_width: self.monospace_width, tab_width: self.tab_width, } } } impl Buffer { /// Create an empty [`Buffer`] with the provided [`Metrics`]. /// This is useful for initializing a [`Buffer`] without a [`FontSystem`]. /// /// You must populate the [`Buffer`] with at least one [`BufferLine`] before shaping and layout, /// for example by calling [`Buffer::set_text`]. /// /// If you have a [`FontSystem`] in scope, you should use [`Buffer::new`] instead. /// /// # Panics /// /// Will panic if `metrics.line_height` is zero. pub fn new_empty(metrics: Metrics) -> Self { assert_ne!(metrics.line_height, 0.0, "line height cannot be 0"); Self { lines: Vec::new(), metrics, width_opt: None, height_opt: None, scroll: Scroll::default(), redraw: false, wrap: Wrap::WordOrGlyph, monospace_width: None, tab_width: 8, } } /// Create a new [`Buffer`] with the provided [`FontSystem`] and [`Metrics`] /// /// # Panics /// /// Will panic if `metrics.line_height` is zero. pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self { let mut buffer = Self::new_empty(metrics); buffer.set_text(font_system, "", Attrs::new(), Shaping::Advanced); buffer } /// Mutably borrows the buffer together with an [`FontSystem`] for more convenient methods pub fn borrow_with<'a>( &'a mut self, font_system: &'a mut FontSystem, ) -> BorrowedWithFontSystem<'a, Buffer> { BorrowedWithFontSystem { inner: self, font_system, } } fn relayout(&mut self, font_system: &mut FontSystem) { #[cfg(all(feature = "std", not(target_arch = "wasm32")))] let instant = std::time::Instant::now(); for line in &mut self.lines { if line.shape_opt().is_some() { line.reset_layout(); line.layout( font_system, self.metrics.font_size, self.width_opt, self.wrap, self.monospace_width, self.tab_width, ); } } self.redraw = true; #[cfg(all(feature = "std", not(target_arch = "wasm32")))] log::debug!("relayout: {:?}", instant.elapsed()); } /// Shape lines until cursor, also scrolling to include cursor in view pub fn shape_until_cursor( &mut self, font_system: &mut FontSystem, cursor: Cursor, prune: bool, ) { let metrics = self.metrics; let old_scroll = self.scroll; let layout_cursor = self .layout_cursor(font_system, cursor) .expect("shape_until_cursor invalid cursor"); let mut layout_y = 0.0; let mut total_height = { let layout = self .line_layout(font_system, layout_cursor.line) .expect("shape_until_cursor failed to scroll forwards"); for layout_i in 0..layout_cursor.layout { layout_y += layout[layout_i] .line_height_opt .unwrap_or(metrics.line_height); } layout_y + layout[layout_cursor.layout] .line_height_opt .unwrap_or(metrics.line_height) }; if self.scroll.line > layout_cursor.line || (self.scroll.line == layout_cursor.line && self.scroll.vertical > layout_y) { // Adjust scroll backwards if cursor is before it self.scroll.line = layout_cursor.line; self.scroll.vertical = layout_y; } else if let Some(height) = self.height_opt { // Adjust scroll forwards if cursor is after it let mut line_i = layout_cursor.line; while line_i > self.scroll.line { line_i -= 1; let layout = self .line_layout(font_system, line_i) .expect("shape_until_cursor failed to scroll forwards"); for layout_line in layout.iter() { total_height += layout_line.line_height_opt.unwrap_or(metrics.line_height); } if total_height > height + self.scroll.vertical { self.scroll.line = line_i; self.scroll.vertical = total_height - height; } } } if old_scroll != self.scroll { self.redraw = true; } self.shape_until_scroll(font_system, prune); // Adjust horizontal scroll to include cursor if let Some(layout_cursor) = self.layout_cursor(font_system, cursor) { if let Some(layout_lines) = self.line_layout(font_system, layout_cursor.line) { if let Some(layout_line) = layout_lines.get(layout_cursor.layout) { let (x_min, x_max) = if let Some(glyph) = layout_line .glyphs .get(layout_cursor.glyph) .or_else(|| layout_line.glyphs.last()) { //TODO: use code from cursor_glyph_opt? let x_a = glyph.x; let x_b = glyph.x + glyph.w; (x_a.min(x_b), x_a.max(x_b)) } else { (0.0, 0.0) }; if x_min < self.scroll.horizontal { self.scroll.horizontal = x_min; self.redraw = true; } if let Some(width) = self.width_opt { if x_max > self.scroll.horizontal + width { self.scroll.horizontal = x_max - width; self.redraw = true; } } } } } } /// Shape lines until scroll pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) { let metrics = self.metrics; let old_scroll = self.scroll; loop { // Adjust scroll.layout to be positive by moving scroll.line backwards while self.scroll.vertical < 0.0 { if self.scroll.line > 0 { let line_i = self.scroll.line - 1; if let Some(layout) = self.line_layout(font_system, line_i) { let mut layout_height = 0.0; for layout_line in layout.iter() { layout_height += layout_line.line_height_opt.unwrap_or(metrics.line_height); } self.scroll.line = line_i; self.scroll.vertical += layout_height; } else { // If layout is missing, just assume line height self.scroll.line = line_i; self.scroll.vertical += metrics.line_height; } } else { self.scroll.vertical = 0.0; break; } } let scroll_start = self.scroll.vertical; let scroll_end = scroll_start + self.height_opt.unwrap_or(f32::INFINITY); let mut total_height = 0.0; for line_i in 0..self.lines.len() { if line_i < self.scroll.line { if prune { self.lines[line_i].reset_shaping(); } continue; } if total_height > scroll_end { if prune { self.lines[line_i].reset_shaping(); continue; } else { break; } } let mut layout_height = 0.0; let layout = self .line_layout(font_system, line_i) .expect("shape_until_scroll invalid line"); for layout_line in layout.iter() { let line_height = layout_line.line_height_opt.unwrap_or(metrics.line_height); layout_height += line_height; total_height += line_height; } // Adjust scroll.vertical to be smaller by moving scroll.line forwards //TODO: do we want to adjust it exactly to a layout line? if line_i == self.scroll.line && layout_height < self.scroll.vertical { self.scroll.line += 1; self.scroll.vertical -= layout_height; } } if total_height < scroll_end && self.scroll.line > 0 { // Need to scroll up to stay inside of buffer self.scroll.vertical -= scroll_end - total_height; } else { // Done adjusting scroll break; } } if old_scroll != self.scroll { self.redraw = true; } } /// Convert a [`Cursor`] to a [`LayoutCursor`] pub fn layout_cursor( &mut self, font_system: &mut FontSystem, cursor: Cursor, ) -> Option { let layout = self.line_layout(font_system, cursor.line)?; for (layout_i, layout_line) in layout.iter().enumerate() { for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() { let cursor_end = Cursor::new_with_affinity(cursor.line, glyph.end, Affinity::Before); let cursor_start = Cursor::new_with_affinity(cursor.line, glyph.start, Affinity::After); let (cursor_left, cursor_right) = if glyph.level.is_ltr() { (cursor_start, cursor_end) } else { (cursor_end, cursor_start) }; if cursor == cursor_left { return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i)); } if cursor == cursor_right { return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i + 1)); } } } // Fall back to start of line //TODO: should this be the end of the line? Some(LayoutCursor::new(cursor.line, 0, 0)) } /// Shape the provided line index and return the result pub fn line_shape( &mut self, font_system: &mut FontSystem, line_i: usize, ) -> Option<&ShapeLine> { let line = self.lines.get_mut(line_i)?; Some(line.shape(font_system, self.tab_width)) } /// Lay out the provided line index and return the result pub fn line_layout( &mut self, font_system: &mut FontSystem, line_i: usize, ) -> Option<&[LayoutLine]> { let line = self.lines.get_mut(line_i)?; Some(line.layout( font_system, self.metrics.font_size, self.width_opt, self.wrap, self.monospace_width, self.tab_width, )) } /// Get the current [`Metrics`] pub fn metrics(&self) -> Metrics { self.metrics } /// Set the current [`Metrics`] /// /// # Panics /// /// Will panic if `metrics.font_size` is zero. pub fn set_metrics(&mut self, font_system: &mut FontSystem, metrics: Metrics) { self.set_metrics_and_size(font_system, metrics, self.width_opt, self.height_opt); } /// Get the current [`Wrap`] pub fn wrap(&self) -> Wrap { self.wrap } /// Set the current [`Wrap`] pub fn set_wrap(&mut self, font_system: &mut FontSystem, wrap: Wrap) { if wrap != self.wrap { self.wrap = wrap; self.relayout(font_system); self.shape_until_scroll(font_system, false); } } /// Get the current `monospace_width` pub fn monospace_width(&self) -> Option { self.monospace_width } /// Set monospace width monospace glyphs should be resized to match. `None` means don't resize pub fn set_monospace_width( &mut self, font_system: &mut FontSystem, monospace_width: Option, ) { if monospace_width != self.monospace_width { self.monospace_width = monospace_width; self.relayout(font_system); self.shape_until_scroll(font_system, false); } } /// Get the current `tab_width` pub fn tab_width(&self) -> u16 { self.tab_width } /// Set tab width (number of spaces between tab stops) pub fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) { // A tab width of 0 is not allowed if tab_width == 0 { return; } if tab_width != self.tab_width { self.tab_width = tab_width; // Shaping must be reset when tab width is changed for line in self.lines.iter_mut() { if line.shape_opt().is_some() { if line.text().contains('\t') { line.reset_shaping(); } } } self.redraw = true; self.shape_until_scroll(font_system, false); } } /// Get the current buffer dimensions (width, height) pub fn size(&self) -> (Option, Option) { (self.width_opt, self.height_opt) } /// Set the current buffer dimensions pub fn set_size( &mut self, font_system: &mut FontSystem, width_opt: Option, height_opt: Option, ) { self.set_metrics_and_size(font_system, self.metrics, width_opt, height_opt); } /// Set the current [`Metrics`] and buffer dimensions at the same time /// /// # Panics /// /// Will panic if `metrics.font_size` is zero. pub fn set_metrics_and_size( &mut self, font_system: &mut FontSystem, metrics: Metrics, width_opt: Option, height_opt: Option, ) { let clamped_width_opt = width_opt.map(|width| width.max(0.0)); let clamped_height_opt = height_opt.map(|height| height.max(0.0)); if metrics != self.metrics || clamped_width_opt != self.width_opt || clamped_height_opt != self.height_opt { assert_ne!(metrics.font_size, 0.0, "font size cannot be 0"); self.metrics = metrics; self.width_opt = clamped_width_opt; self.height_opt = clamped_height_opt; self.relayout(font_system); self.shape_until_scroll(font_system, false); } } /// Get the current scroll location pub fn scroll(&self) -> Scroll { self.scroll } /// Set the current scroll location pub fn set_scroll(&mut self, scroll: Scroll) { if scroll != self.scroll { self.scroll = scroll; self.redraw = true; } } /// Set text of buffer, using provided attributes for each line by default pub fn set_text( &mut self, font_system: &mut FontSystem, text: &str, attrs: Attrs, shaping: Shaping, ) { self.lines.clear(); for (range, ending) in LineIter::new(text) { self.lines.push(BufferLine::new( &text[range], ending, AttrsList::new(attrs), shaping, )); } if self.lines.is_empty() { self.lines.push(BufferLine::new( "", LineEnding::default(), AttrsList::new(attrs), shaping, )); } self.scroll = Scroll::default(); self.shape_until_scroll(font_system, false); } /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) /// /// ``` /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; /// # let mut font_system = FontSystem::new(); /// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0)); /// let attrs = Attrs::new().family(Family::Serif); /// buffer.set_rich_text( /// &mut font_system, /// [ /// ("hello, ", attrs), /// ("cosmic\ntext", attrs.family(Family::Monospace)), /// ], /// attrs, /// Shaping::Advanced, /// None, /// ); /// ``` pub fn set_rich_text<'r, 's, I>( &mut self, font_system: &mut FontSystem, spans: I, default_attrs: Attrs, shaping: Shaping, alignment: Option, ) where I: IntoIterator)>, { let mut end = 0; // TODO: find a way to cache this string and vec for reuse let (string, spans_data): (String, Vec<_>) = spans .into_iter() .map(|(s, attrs)| { let start = end; end += s.len(); (s, (attrs, start..end)) }) .unzip(); let mut spans_iter = spans_data.into_iter(); let mut maybe_span = spans_iter.next(); // split the string into lines, as ranges let string_start = string.as_ptr() as usize; let mut lines_iter = BidiParagraphs::new(&string).map(|line: &str| { let start = line.as_ptr() as usize - string_start; let end = start + line.len(); start..end }); let mut maybe_line = lines_iter.next(); //TODO: set this based on information from spans let line_ending = LineEnding::default(); let mut line_count = 0; let mut attrs_list = self .lines .get_mut(line_count) .map(BufferLine::reclaim_attrs) .unwrap_or_else(|| AttrsList::new(Attrs::new())) .reset(default_attrs); let mut line_string = self .lines .get_mut(line_count) .map(BufferLine::reclaim_text) .unwrap_or_default(); loop { let (Some(line_range), Some((attrs, span_range))) = (&maybe_line, &maybe_span) else { // this is reached only if this text is empty if self.lines.len() == line_count { self.lines.push(BufferLine::empty()); } self.lines[line_count].reset_new( String::new(), line_ending, AttrsList::new(default_attrs), shaping, ); line_count += 1; break; }; // start..end is the intersection of this line and this span let start = line_range.start.max(span_range.start); let end = line_range.end.min(span_range.end); if start < end { let text = &string[start..end]; let text_start = line_string.len(); line_string.push_str(text); let text_end = line_string.len(); // Only add attrs if they don't match the defaults if *attrs != attrs_list.defaults() { attrs_list.add_span(text_start..text_end, *attrs); } } // we know that at the end of a line, // span text's end index is always >= line text's end index // so if this span ends before this line ends, // there is another span in this line. // otherwise, we move on to the next line. if span_range.end < line_range.end { maybe_span = spans_iter.next(); } else { maybe_line = lines_iter.next(); if maybe_line.is_some() { // finalize this line and start a new line let next_attrs_list = self .lines .get_mut(line_count + 1) .map(BufferLine::reclaim_attrs) .unwrap_or_else(|| AttrsList::new(Attrs::new())) .reset(default_attrs); let next_line_string = self .lines .get_mut(line_count + 1) .map(BufferLine::reclaim_text) .unwrap_or_default(); let prev_attrs_list = core::mem::replace(&mut attrs_list, next_attrs_list); let prev_line_string = core::mem::replace(&mut line_string, next_line_string); if self.lines.len() == line_count { self.lines.push(BufferLine::empty()); } self.lines[line_count].reset_new( prev_line_string, line_ending, prev_attrs_list, shaping, ); line_count += 1; } else { // finalize the final line if self.lines.len() == line_count { self.lines.push(BufferLine::empty()); } self.lines[line_count].reset_new(line_string, line_ending, attrs_list, shaping); line_count += 1; break; } } } // Discard excess lines now that we have reused as much of the existing allocations as possible. self.lines.truncate(line_count); self.lines.iter_mut().for_each(|line| { line.set_align(alignment); }); self.scroll = Scroll::default(); self.shape_until_scroll(font_system, false); } /// True if a redraw is needed pub fn redraw(&self) -> bool { self.redraw } /// Set redraw needed flag pub fn set_redraw(&mut self, redraw: bool) { self.redraw = redraw; } /// Get the visible layout runs for rendering and other tasks pub fn layout_runs(&self) -> LayoutRunIter { LayoutRunIter::new(self) } /// Convert x, y position to Cursor (hit detection) pub fn hit(&self, x: f32, y: f32) -> Option { #[cfg(all(feature = "std", not(target_arch = "wasm32")))] let instant = std::time::Instant::now(); let mut new_cursor_opt = None; let mut runs = self.layout_runs().peekable(); let mut first_run = true; while let Some(run) = runs.next() { let line_top = run.line_top; let line_height = run.line_height; if first_run && y < line_top { first_run = false; let new_cursor = Cursor::new(run.line_i, 0); new_cursor_opt = Some(new_cursor); } else if y >= line_top && y < line_top + line_height { let mut new_cursor_glyph = run.glyphs.len(); let mut new_cursor_char = 0; let mut new_cursor_affinity = Affinity::After; let mut first_glyph = true; 'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() { if first_glyph { first_glyph = false; if (run.rtl && x > glyph.x) || (!run.rtl && x < 0.0) { new_cursor_glyph = 0; new_cursor_char = 0; } } if x >= glyph.x && x <= glyph.x + glyph.w { new_cursor_glyph = glyph_i; let cluster = &run.text[glyph.start..glyph.end]; let total = cluster.grapheme_indices(true).count(); let mut egc_x = glyph.x; let egc_w = glyph.w / (total as f32); for (egc_i, egc) in cluster.grapheme_indices(true) { if x >= egc_x && x <= egc_x + egc_w { new_cursor_char = egc_i; let right_half = x >= egc_x + egc_w / 2.0; if right_half != glyph.level.is_rtl() { // If clicking on last half of glyph, move cursor past glyph new_cursor_char += egc.len(); new_cursor_affinity = Affinity::Before; } break 'hit; } egc_x += egc_w; } let right_half = x >= glyph.x + glyph.w / 2.0; if right_half != glyph.level.is_rtl() { // If clicking on last half of glyph, move cursor past glyph new_cursor_char = cluster.len(); new_cursor_affinity = Affinity::Before; } break 'hit; } } let mut new_cursor = Cursor::new(run.line_i, 0); match run.glyphs.get(new_cursor_glyph) { Some(glyph) => { // Position at glyph new_cursor.index = glyph.start + new_cursor_char; new_cursor.affinity = new_cursor_affinity; } None => { if let Some(glyph) = run.glyphs.last() { // Position at end of line new_cursor.index = glyph.end; new_cursor.affinity = Affinity::Before; } } } new_cursor_opt = Some(new_cursor); break; } else if runs.peek().is_none() && y > run.line_y { let mut new_cursor = Cursor::new(run.line_i, 0); if let Some(glyph) = run.glyphs.last() { new_cursor = run.cursor_from_glyph_right(glyph); } new_cursor_opt = Some(new_cursor); } } #[cfg(all(feature = "std", not(target_arch = "wasm32")))] log::trace!("click({}, {}): {:?}", x, y, instant.elapsed()); new_cursor_opt } /// Apply a [`Motion`] to a [`Cursor`] pub fn cursor_motion( &mut self, font_system: &mut FontSystem, mut cursor: Cursor, mut cursor_x_opt: Option, motion: Motion, ) -> Option<(Cursor, Option)> { match motion { Motion::LayoutCursor(layout_cursor) => { let layout = self.line_layout(font_system, layout_cursor.line)?; let layout_line = match layout.get(layout_cursor.layout) { Some(some) => some, None => match layout.last() { Some(some) => some, None => { return None; } }, }; let (new_index, new_affinity) = match layout_line.glyphs.get(layout_cursor.glyph) { Some(glyph) => (glyph.start, Affinity::After), None => match layout_line.glyphs.last() { Some(glyph) => (glyph.end, Affinity::Before), //TODO: is this correct? None => (0, Affinity::After), }, }; if cursor.line != layout_cursor.line || cursor.index != new_index || cursor.affinity != new_affinity { cursor.line = layout_cursor.line; cursor.index = new_index; cursor.affinity = new_affinity; } } Motion::Previous => { let line = self.lines.get(cursor.line)?; if cursor.index > 0 { // Find previous character index let mut prev_index = 0; for (i, _) in line.text().grapheme_indices(true) { if i < cursor.index { prev_index = i; } else { break; } } cursor.index = prev_index; cursor.affinity = Affinity::After; } else if cursor.line > 0 { cursor.line -= 1; cursor.index = self.lines.get(cursor.line)?.text().len(); cursor.affinity = Affinity::After; } cursor_x_opt = None; } Motion::Next => { let line = self.lines.get(cursor.line)?; if cursor.index < line.text().len() { for (i, c) in line.text().grapheme_indices(true) { if i == cursor.index { cursor.index += c.len(); cursor.affinity = Affinity::Before; break; } } } else if cursor.line + 1 < self.lines.len() { cursor.line += 1; cursor.index = 0; cursor.affinity = Affinity::Before; } cursor_x_opt = None; } Motion::Left => { let rtl_opt = self .line_shape(font_system, cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { (cursor, cursor_x_opt) = self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Next)?; } else { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::Previous, )?; } } } Motion::Right => { let rtl_opt = self .line_shape(font_system, cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::Previous, )?; } else { (cursor, cursor_x_opt) = self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Next)?; } } } Motion::Up => { let mut layout_cursor = self.layout_cursor(font_system, cursor)?; if cursor_x_opt.is_none() { cursor_x_opt = Some( layout_cursor.glyph as i32, //TODO: glyph x position ); } if layout_cursor.layout > 0 { layout_cursor.layout -= 1; } else if layout_cursor.line > 0 { layout_cursor.line -= 1; layout_cursor.layout = usize::max_value(); } if let Some(cursor_x) = cursor_x_opt { layout_cursor.glyph = cursor_x as usize; //TODO: glyph x position } (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::LayoutCursor(layout_cursor), )?; } Motion::Down => { let mut layout_cursor = self.layout_cursor(font_system, cursor)?; let layout_len = self.line_layout(font_system, layout_cursor.line)?.len(); if cursor_x_opt.is_none() { cursor_x_opt = Some( layout_cursor.glyph as i32, //TODO: glyph x position ); } if layout_cursor.layout + 1 < layout_len { layout_cursor.layout += 1; } else if layout_cursor.line + 1 < self.lines.len() { layout_cursor.line += 1; layout_cursor.layout = 0; } if let Some(cursor_x) = cursor_x_opt { layout_cursor.glyph = cursor_x as usize; //TODO: glyph x position } (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::LayoutCursor(layout_cursor), )?; } Motion::Home => { let mut layout_cursor = self.layout_cursor(font_system, cursor)?; layout_cursor.glyph = 0; #[allow(unused_assignments)] { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::LayoutCursor(layout_cursor), )?; } cursor_x_opt = None; } Motion::SoftHome => { let line = self.lines.get(cursor.line)?; cursor.index = line .text() .char_indices() .filter_map(|(i, c)| if c.is_whitespace() { None } else { Some(i) }) .next() .unwrap_or(0); cursor_x_opt = None; } Motion::End => { let mut layout_cursor = self.layout_cursor(font_system, cursor)?; layout_cursor.glyph = usize::max_value(); #[allow(unused_assignments)] { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::LayoutCursor(layout_cursor), )?; } cursor_x_opt = None; } Motion::ParagraphStart => { cursor.index = 0; cursor_x_opt = None; } Motion::ParagraphEnd => { cursor.index = self.lines.get(cursor.line)?.text().len(); cursor_x_opt = None; } Motion::PageUp => { if let Some(height) = self.height_opt { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::Vertical(-height as i32), )?; } } Motion::PageDown => { if let Some(height) = self.height_opt { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::Vertical(height as i32), )?; } } Motion::Vertical(px) => { // TODO more efficient, use layout run line height let lines = px / self.metrics().line_height as i32; match lines.cmp(&0) { cmp::Ordering::Less => { for _ in 0..-lines { (cursor, cursor_x_opt) = self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Up)?; } } cmp::Ordering::Greater => { for _ in 0..lines { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::Down, )?; } } cmp::Ordering::Equal => {} } } Motion::PreviousWord => { let line = self.lines.get(cursor.line)?; if cursor.index > 0 { cursor.index = line .text() .unicode_word_indices() .rev() .map(|(i, _)| i) .find(|&i| i < cursor.index) .unwrap_or(0); } else if cursor.line > 0 { cursor.line -= 1; cursor.index = self.lines.get(cursor.line)?.text().len(); } cursor_x_opt = None; } Motion::NextWord => { let line = self.lines.get(cursor.line)?; if cursor.index < line.text().len() { cursor.index = line .text() .unicode_word_indices() .map(|(i, word)| i + word.len()) .find(|&i| i > cursor.index) .unwrap_or(line.text().len()); } else if cursor.line + 1 < self.lines.len() { cursor.line += 1; cursor.index = 0; } cursor_x_opt = None; } Motion::LeftWord => { let rtl_opt = self .line_shape(font_system, cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::NextWord, )?; } else { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::PreviousWord, )?; } } } Motion::RightWord => { let rtl_opt = self .line_shape(font_system, cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::PreviousWord, )?; } else { (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::NextWord, )?; } } } Motion::BufferStart => { cursor.line = 0; cursor.index = 0; cursor_x_opt = None; } Motion::BufferEnd => { cursor.line = self.lines.len().saturating_sub(1); cursor.index = self.lines.get(cursor.line)?.text().len(); cursor_x_opt = None; } Motion::GotoLine(line) => { let mut layout_cursor = self.layout_cursor(font_system, cursor)?; layout_cursor.line = line; (cursor, cursor_x_opt) = self.cursor_motion( font_system, cursor, cursor_x_opt, Motion::LayoutCursor(layout_cursor), )?; } } Some((cursor, cursor_x_opt)) } /// Draw the buffer #[cfg(feature = "swash")] pub fn draw( &self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, color: Color, mut f: F, ) where F: FnMut(i32, i32, u32, u32, Color), { for run in self.layout_runs() { for glyph in run.glyphs.iter() { let physical_glyph = glyph.physical((0., 0.), 1.0); let glyph_color = match glyph.color_opt { Some(some) => some, None => color, }; cache.with_pixels( font_system, physical_glyph.cache_key, glyph_color, |x, y, color| { f( physical_glyph.x + x, run.line_y as i32 + physical_glyph.y + y, 1, 1, color, ); }, ); } } } } impl<'a> BorrowedWithFontSystem<'a, Buffer> { /// Shape lines until cursor, also scrolling to include cursor in view pub fn shape_until_cursor(&mut self, cursor: Cursor, prune: bool) { self.inner .shape_until_cursor(self.font_system, cursor, prune); } /// Shape lines until scroll pub fn shape_until_scroll(&mut self, prune: bool) { self.inner.shape_until_scroll(self.font_system, prune); } /// Shape the provided line index and return the result pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> { self.inner.line_shape(self.font_system, line_i) } /// Lay out the provided line index and return the result pub fn line_layout(&mut self, line_i: usize) -> Option<&[LayoutLine]> { self.inner.line_layout(self.font_system, line_i) } /// Set the current [`Metrics`] /// /// # Panics /// /// Will panic if `metrics.font_size` is zero. pub fn set_metrics(&mut self, metrics: Metrics) { self.inner.set_metrics(self.font_system, metrics); } /// Set the current [`Wrap`] pub fn set_wrap(&mut self, wrap: Wrap) { self.inner.set_wrap(self.font_system, wrap); } /// Set the current buffer dimensions pub fn set_size(&mut self, width_opt: Option, height_opt: Option) { self.inner.set_size(self.font_system, width_opt, height_opt); } /// Set the current [`Metrics`] and buffer dimensions at the same time /// /// # Panics /// /// Will panic if `metrics.font_size` is zero. pub fn set_metrics_and_size( &mut self, metrics: Metrics, width_opt: Option, height_opt: Option, ) { self.inner .set_metrics_and_size(self.font_system, metrics, width_opt, height_opt); } /// Set tab width (number of spaces between tab stops) pub fn set_tab_width(&mut self, tab_width: u16) { self.inner.set_tab_width(self.font_system, tab_width); } /// Set text of buffer, using provided attributes for each line by default pub fn set_text(&mut self, text: &str, attrs: Attrs, shaping: Shaping) { self.inner.set_text(self.font_system, text, attrs, shaping); } /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) /// /// ``` /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; /// # let mut font_system = FontSystem::new(); /// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0)); /// let mut buffer = buffer.borrow_with(&mut font_system); /// let attrs = Attrs::new().family(Family::Serif); /// buffer.set_rich_text( /// [ /// ("hello, ", attrs), /// ("cosmic\ntext", attrs.family(Family::Monospace)), /// ], /// attrs, /// Shaping::Advanced, /// None, /// ); /// ``` pub fn set_rich_text<'r, 's, I>( &mut self, spans: I, default_attrs: Attrs, shaping: Shaping, alignment: Option, ) where I: IntoIterator)>, { self.inner .set_rich_text(self.font_system, spans, default_attrs, shaping, alignment); } /// Apply a [`Motion`] to a [`Cursor`] pub fn cursor_motion( &mut self, cursor: Cursor, cursor_x_opt: Option, motion: Motion, ) -> Option<(Cursor, Option)> { self.inner .cursor_motion(self.font_system, cursor, cursor_x_opt, motion) } /// Draw the buffer #[cfg(feature = "swash")] pub fn draw(&mut self, cache: &mut crate::SwashCache, color: Color, f: F) where F: FnMut(i32, i32, u32, u32, Color), { self.inner.draw(self.font_system, cache, color, f); } }