diff --git a/examples/editor-libcosmic/src/main.rs b/examples/editor-libcosmic/src/main.rs index ca7749d..5e61aad 100644 --- a/examples/editor-libcosmic/src/main.rs +++ b/examples/editor-libcosmic/src/main.rs @@ -24,6 +24,8 @@ use cosmic::{ }, }; use cosmic_text::{ + Attrs, + AttrsList, FontSystem, SwashCache, TextBuffer, @@ -63,11 +65,9 @@ fn main() -> cosmic::iced::Result { pub struct Window { theme: Theme, path_opt: Option, + attrs: Attrs<'static>, buffer: Mutex>, cache: Mutex>, - bold: bool, - italic: bool, - monospaced: bool, } #[allow(dead_code)] @@ -88,12 +88,12 @@ impl Window { match fs::read_to_string(&path) { Ok(text) => { log::info!("opened '{}'", path.display()); - buffer.set_text(&text); + buffer.set_text(&text, self.attrs); self.path_opt = Some(path); }, Err(err) => { log::error!("failed to open '{}': {}", path.display(), err); - buffer.set_text(""); + buffer.set_text("", self.attrs); self.path_opt = None; } } @@ -107,14 +107,13 @@ impl Application for Window { type Theme = Theme; fn new(_flags: ()) -> (Self, Command) { - let font_size_i = 1; // Body let attrs = cosmic_text::Attrs::new() .monospaced(true) .family(cosmic_text::Family::Monospace); + let buffer = TextBuffer::new( &FONT_SYSTEM, - attrs, - FONT_SIZES[font_size_i], + FONT_SIZES[1 /* Body */], ); let cache = SwashCache::new(&FONT_SYSTEM); @@ -122,11 +121,9 @@ impl Application for Window { let mut window = Window { theme: Theme::Dark, path_opt: None, + attrs, buffer: Mutex::new(buffer), cache: Mutex::new(cache), - bold: false, - italic: false, - monospaced: true, }; if let Some(arg) = env::args().nth(1) { window.open(PathBuf::from(arg)); @@ -157,7 +154,7 @@ impl Application for Window { if let Some(path) = &self.path_opt { let buffer = self.buffer.lock().unwrap(); let mut text = String::new(); - for line in buffer.text_lines() { + for line in buffer.lines.iter() { text.push_str(line.text()); text.push('\n'); } @@ -172,39 +169,42 @@ impl Application for Window { } }, Message::Bold(bold) => { - self.bold = bold; - - let mut buffer = self.buffer.lock().unwrap(); - let attrs = buffer.attrs().clone().weight(if bold { + self.attrs = self.attrs.weight(if bold { cosmic_text::Weight::BOLD } else { cosmic_text::Weight::NORMAL }); - buffer.set_attrs(attrs); - }, - Message::Italic(italic) => { - self.italic = italic; let mut buffer = self.buffer.lock().unwrap(); - let attrs = buffer.attrs().clone().style(if italic { + for line in buffer.lines.iter_mut() { + line.set_attrs_list(AttrsList::new(self.attrs)); + } + }, + Message::Italic(italic) => { + self.attrs = self.attrs.style(if italic { cosmic_text::Style::Italic } else { cosmic_text::Style::Normal }); - buffer.set_attrs(attrs); - }, - Message::Monospaced(monospaced) => { - self.monospaced = monospaced; let mut buffer = self.buffer.lock().unwrap(); - let attrs = buffer.attrs().clone() + for line in buffer.lines.iter_mut() { + line.set_attrs_list(AttrsList::new(self.attrs)); + } + }, + Message::Monospaced(monospaced) => { + self.attrs = self.attrs .family(if monospaced { cosmic_text::Family::Monospace } else { cosmic_text::Family::SansSerif }) .monospaced(monospaced); - buffer.set_attrs(attrs); + + let mut buffer = self.buffer.lock().unwrap(); + for line in buffer.lines.iter_mut() { + line.set_attrs_list(AttrsList::new(self.attrs)); + } }, Message::MetricsChanged(metrics) => { let mut buffer = self.buffer.lock().unwrap(); @@ -246,11 +246,11 @@ impl Application for Window { button!("Save").on_press(Message::Save), horizontal_space(Length::Fill), text("Bold:"), - toggler(None, self.bold, Message::Bold), + toggler(None, self.attrs.weight == cosmic_text::Weight::BOLD, Message::Bold), text("Italic:"), - toggler(None, self.italic, Message::Italic), + toggler(None, self.attrs.style == cosmic_text::Style::Italic, Message::Italic), text("Monospaced:"), - toggler(None, self.monospaced, Message::Monospaced), + toggler(None, self.attrs.monospaced, Message::Monospaced), text("Theme:"), theme_picker, text("Font Size:"), diff --git a/examples/editor-libcosmic/src/text_box.rs b/examples/editor-libcosmic/src/text_box.rs index 932c0cb..25382ca 100644 --- a/examples/editor-libcosmic/src/text_box.rs +++ b/examples/editor-libcosmic/src/text_box.rs @@ -179,27 +179,6 @@ where } }); - /* - // Draw scrollbar - { - let start_line = start_line_opt.unwrap_or(end_line); - let lines = buffer.text_lines().len(); - let start_y = (start_line.get() * window.height() as usize) / lines; - let end_y = (end_line.get() * window.height() as usize) / lines; - if end_y > start_y { - window.rect( - window.width() as i32 - line_x as i32, - start_y as i32, - line_x as u32, - (end_y - start_y) as u32, - Color::from_rgba8(0xFF, 0xFF, 0xFF, 0.25), - ); - } - } - - buffer.redraw = false; - */ - let duration = instant.elapsed(); log::trace!("redraw: {:?}", duration); } diff --git a/examples/editor-orbclient/src/main.rs b/examples/editor-orbclient/src/main.rs index 78f1324..4fd0a03 100644 --- a/examples/editor-orbclient/src/main.rs +++ b/examples/editor-orbclient/src/main.rs @@ -78,12 +78,8 @@ fn main() { let mut font_size_i = font_size_default; let line_x = 8 * display_scale; - let attrs = Attrs::new() - .monospaced(true) - .family(Family::Monospace); let mut buffer = TextBuffer::new( &font_system, - attrs, font_sizes[font_size_i] ); @@ -92,7 +88,10 @@ fn main() { window.height() as i32 ); - buffer.set_text(&text); + let attrs = Attrs::new() + .monospaced(true) + .family(Family::Monospace); + buffer.set_text(&text, attrs); let mut bg_color = orbclient::Color::rgb(0x00, 0x00, 0x00); let mut font_color = Color::rgb(0xFF, 0xFF, 0xFF); @@ -176,8 +175,7 @@ fn main() { let mut attrs_list = AttrsList::new(attrs); for (style, _, range) in ranges { attrs_list.add_span( - range.start, - range.end, + range, attrs .color(Color::rgba( style.foreground.r, @@ -242,19 +240,19 @@ fn main() { window.rect(line_x + x, y, w, h, orbclient::Color { data: color.0 }) }); - let mut start_line_opt = None; - let mut end_line = 0; - for run in buffer.layout_runs() { - end_line = run.line_i; - if start_line_opt == None { - start_line_opt = Some(end_line); - } - } - // Draw scrollbar { + let mut start_line_opt = None; + let mut end_line = 0; + for run in buffer.layout_runs() { + end_line = run.line_i; + if start_line_opt == None { + start_line_opt = Some(end_line); + } + } + let start_line = start_line_opt.unwrap_or(end_line); - let lines = buffer.text_lines().len(); + let lines = buffer.lines.len(); let start_y = (start_line * window.height() as usize) / lines; let end_y = (end_line * window.height() as usize) / lines; if end_y > start_y { diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index 5bae1db..696bd08 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -31,10 +31,8 @@ fn main() { ) .unwrap(); - let attrs = Attrs::new(); let mut buffer = TextBuffer::new( &font_system, - attrs, TextMetrics::new(32, 44).scale(display_scale) ); @@ -43,6 +41,7 @@ fn main() { window.height() as i32 ); + let attrs = Attrs::new(); let serif_attrs = attrs.family(Family::Serif); let mono_attrs = attrs.monospaced(true).family(Family::Monospace); let comic_attrs = attrs.family(Family::Name("Comic Neue")); @@ -117,7 +116,7 @@ fn main() { let start = line_text.len(); line_text.push_str(text); let end = line_text.len(); - attrs_list.add_span(start, end, attrs); + attrs_list.add_span(start..end, attrs); } buffer.lines.push(TextBufferLine::new(line_text, attrs_list)); } diff --git a/src/attrs.rs b/src/attrs.rs index 7ce39ab..a97e3eb 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 +use std::ops::Range; + pub use fontdb::{Family, Stretch, Style, Weight}; +/// Text color #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Color(pub u32); @@ -48,6 +51,7 @@ impl Color { } } +/// Text attributes #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Attrs<'a> { //TODO: should this be an option? @@ -60,6 +64,9 @@ pub struct Attrs<'a> { } impl<'a> Attrs<'a> { + /// Create a new set of attributes with sane defaults + /// + /// This defaults to a regular Sans-Serif font. pub fn new() -> Self { Self { color_opt: None, @@ -71,36 +78,43 @@ impl<'a> Attrs<'a> { } } + /// Set [Color] pub fn color(mut self, color: Color) -> Self { self.color_opt = Some(color); self } + /// Set [Family] pub fn family(mut self, family: Family<'a>) -> Self { self.family = family; self } + /// Set monospaced pub fn monospaced(mut self, monospaced: bool) -> Self { self.monospaced = monospaced; self } + /// Set [Stretch] pub fn stretch(mut self, stretch: Stretch) -> Self { self.stretch = stretch; self } + /// Set [Style] pub fn style(mut self, style: Style) -> Self { self.style = style; self } + /// Set [Weight] pub fn weight(mut self, weight: Weight) -> Self { self.weight = weight; self } + /// Check if font matches pub fn matches(&self, face: &fontdb::FaceInfo) -> bool { //TODO: smarter way of including emoji face.post_script_name.contains("Emoji") || @@ -112,6 +126,7 @@ impl<'a> Attrs<'a> { ) } + /// Check if this set of attributes can be shaped with another pub fn compatible(&self, other: &Self) -> bool { self.family == other.family && self.monospaced == other.monospaced @@ -121,13 +136,15 @@ impl<'a> Attrs<'a> { } } +/// List of text attributes to apply to a line #[derive(Eq, PartialEq)] pub struct AttrsList<'a> { defaults: Attrs<'a>, - spans: Vec<(usize, usize, Attrs<'a>)>, + spans: Vec<(Range, Attrs<'a>)>, } impl<'a> AttrsList<'a> { + /// Create a new attributes list with a set of default [Attrs] pub fn new(defaults: Attrs<'a>) -> Self { Self { defaults, @@ -135,29 +152,65 @@ impl<'a> AttrsList<'a> { } } + /// Get the default [Attrs] pub fn defaults(&self) -> Attrs<'a> { self.defaults } - pub fn spans(&self) -> &Vec<(usize, usize, Attrs<'a>)> { + /// Get the current attribute spans + pub fn spans(&self) -> &Vec<(Range, Attrs<'a>)> { &self.spans } + /// Clear the current attribute spans pub fn clear_spans(&mut self) { self.spans.clear(); } - pub fn add_span(&mut self, start: usize, end: usize, attrs: Attrs<'a>) { - self.spans.push((start, end, attrs)); + /// Add an attribute span + pub fn add_span(&mut self, range: Range, attrs: Attrs<'a>) { + self.spans.push((range, attrs)); } - pub fn get_span(&self, start: usize, end: usize) -> Attrs<'a> { + /// Get the highest priority attribute span for a range + /// + /// This returns the latest added span that contains the range + pub fn get_span(&self, range: Range) -> Attrs<'a> { let mut attrs = self.defaults; for span in self.spans.iter() { - if start >= span.0 && end <= span.1 { - attrs = span.2; + if range.start >= span.0.start && range.end <= span.0.end { + attrs = span.1; } } attrs } + + /// Split attributes list at an offset + pub fn split_off(&mut self, index: usize) -> Self { + let mut new = Self::new(self.defaults); + let mut i = 0; + while i < self.spans.len() { + if self.spans[i].0.end <= index { + // Leave this in the previous attributes list + i += 1; + } else if self.spans[i].0.start >= index { + // Move this to the new attributes list + let (range, attrs) = self.spans.remove(i); + new.spans.push(( + range.start - index..range.end - index, + attrs + )); + } else { + // New span has index..end + new.spans.push(( + 0..self.spans[i].0.end - index, + self.spans[i].1 + )); + // Old span has start..index + self.spans[i].0.end = index; + i += 1; + } + } + new + } } diff --git a/src/buffer.rs b/src/buffer.rs index c7fb47c..e9ac6fd 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -173,6 +173,7 @@ impl fmt::Display for TextMetrics { } } +/// A line (or paragraph) of text that is shaped and laid out pub struct TextBufferLine<'a> { text: String, attrs_list: AttrsList<'a>, @@ -182,9 +183,12 @@ pub struct TextBufferLine<'a> { } impl<'a> TextBufferLine<'a> { - pub fn new(text: String, attrs_list: AttrsList<'a>) -> Self { + /// Create a new line with the given text and attributes list + /// Cached shaping and layout can be done using the [Self::shape] and + /// [Self::layout] functions + pub fn new>(text: T, attrs_list: AttrsList<'a>) -> Self { Self { - text, + text: text.into(), attrs_list, wrap_simple: false, shape_opt: None, @@ -198,7 +202,8 @@ impl<'a> TextBufferLine<'a> { } /// Set text - /// Will reset shape and layout if it differs from current text + /// + /// Will reset shape and layout if it differs from current text. /// Returns true if the line was reset pub fn set_text + Into>(&mut self, text: T) -> bool { if text.as_ref() != &self.text { @@ -215,8 +220,9 @@ impl<'a> TextBufferLine<'a> { &self.attrs_list } - /// Set attributes list. - /// Will reset shape and layout if it differs from current attributes list + /// Set attributes list + /// + /// Will reset shape and layout if it differs from current attributes list. /// Returns true if the line was reset pub fn set_attrs_list(&mut self, attrs_list: AttrsList<'a>) -> bool { if attrs_list != self.attrs_list { @@ -234,7 +240,8 @@ impl<'a> TextBufferLine<'a> { } /// Set simple wrapping setting (wrap by characters only) - /// Will reset shape and layout if it differs from current simple wrapping setting + /// + /// Will reset shape and layout if it differs from current simple wrapping setting. /// Returns true if the line was reset pub fn set_wrap_simple(&mut self, wrap_simple: bool) -> bool { if wrap_simple != self.wrap_simple { @@ -288,7 +295,7 @@ impl<'a> TextBufferLine<'a> { /// A buffer of text that is shaped and laid out pub struct TextBuffer<'a> { font_system: &'a FontSystem<'a>, - attrs: Attrs<'a>, + /// Lines (or paragraphs) of text in the buffer pub lines: Vec>, metrics: TextMetrics, width: i32, @@ -297,19 +304,22 @@ pub struct TextBuffer<'a> { cursor: TextCursor, cursor_x_opt: Option, select_opt: Option, + /// True if the cursor has been moved. Set to false after processing + /// + /// Usually, if this is true, you should run [Self::shape_until_cursor] before redrawing. + /// Otherwise, you should run [Self::shape_until_scroll] pub cursor_moved: bool, + /// True if a redraw is requires. Set to false after processing pub redraw: bool, } impl<'a> TextBuffer<'a> { pub fn new( font_system: &'a FontSystem<'a>, - attrs: Attrs<'a>, metrics: TextMetrics, ) -> Self { let mut buffer = Self { font_system, - attrs, lines: Vec::new(), metrics, width: 0, @@ -321,7 +331,7 @@ impl<'a> TextBuffer<'a> { cursor_moved: false, redraw: false, }; - buffer.set_text(""); + buffer.set_text("", Attrs::new()); buffer } @@ -400,6 +410,7 @@ impl<'a> TextBuffer<'a> { self.shape_until_scroll(); } + /// Shape lines until scroll pub fn shape_until_scroll(&mut self) { let lines = self.lines(); @@ -558,33 +569,15 @@ impl<'a> TextBuffer<'a> { self.height / self.metrics.line_height } - pub fn attrs(&self) -> &Attrs<'a> { - &self.attrs - } - - /// Set attributes - pub fn set_attrs(&mut self, attrs: Attrs<'a>) { - if attrs != self.attrs { - self.attrs = attrs; - - for line in self.lines.iter_mut() { - line.attrs_list = AttrsList::new(attrs); - line.reset(); - } - - self.shape_until_scroll(); - } - } - - /// Set text of buffer - pub fn set_text(&mut self, text: &str) { + /// Set text of buffer, using provided attributes for each line by default + pub fn set_text(&mut self, text: &str, attrs: Attrs<'a>) { self.lines.clear(); for line in text.lines() { - self.lines.push(TextBufferLine::new(line.to_string(), AttrsList::new(self.attrs))); + self.lines.push(TextBufferLine::new(line.to_string(), AttrsList::new(attrs))); } // Make sure there is always one line if self.lines.is_empty() { - self.lines.push(TextBufferLine::new(String::new(), AttrsList::new(self.attrs))); + self.lines.push(TextBufferLine::new(String::new(), AttrsList::new(attrs))); } self.scroll = 0; @@ -594,11 +587,6 @@ impl<'a> TextBuffer<'a> { self.shape_until_scroll(); } - /// Get the lines of the original text - pub fn text_lines(&self) -> &[TextBufferLine] { - &self.lines - } - /// Perform a [TextAction] on the buffer pub fn action(&mut self, action: TextAction) { let old_cursor = self.cursor; @@ -763,14 +751,16 @@ impl<'a> TextBuffer<'a> { let new_line = { let line = &mut self.lines[self.cursor.line]; line.reset(); - line.text.split_off(self.cursor.index) + TextBufferLine::new( + line.text.split_off(self.cursor.index), + line.attrs_list.split_off(self.cursor.index) + ) }; - let next_line = self.cursor.line + 1; - self.lines.insert(next_line, TextBufferLine::new(new_line, AttrsList::new(self.attrs))); - - self.cursor.line = next_line; + self.cursor.line += 1; self.cursor.index = 0; + + self.lines.insert(self.cursor.line, new_line); }, TextAction::Backspace => { if self.cursor.index > 0 { diff --git a/src/shape.rs b/src/shape.rs index aa0ae8d..f484427 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -94,7 +94,7 @@ fn shape_fallback( // Set color //TODO: these attributes should not be related to shaping for glyph in glyphs.iter_mut() { - let attrs = attrs_list.get_span(glyph.start, glyph.end); + let attrs = attrs_list.get_span(glyph.start..glyph.end); glyph.color_opt = attrs.color_opt; } @@ -129,7 +129,7 @@ fn shape_run<'a>( &line[start_run..end_run], ); - let attrs = attrs_list.get_span(start_run, end_run); + let attrs = attrs_list.get_span(start_run..end_run); let font_matches = font_system.get_font_matches(attrs); @@ -234,6 +234,7 @@ fn shape_run<'a>( glyphs } +/// A shaped glyph pub struct ShapeGlyph { pub start: usize, pub end: usize, @@ -272,6 +273,7 @@ impl ShapeGlyph { } } +/// A shaped word (for word wrapping) pub struct ShapeWord { pub blank: bool, pub glyphs: Vec, @@ -302,7 +304,7 @@ impl ShapeWord { for (egc_i, egc) in word.grapheme_indices(true) { let start_egc = start_word + egc_i; let end_egc = start_egc + egc.len(); - let attrs_egc = attrs_list.get_span(start_egc, end_egc); + let attrs_egc = attrs_list.get_span(start_egc..end_egc); if ! attrs.compatible(&attrs_egc) { //TODO: more efficient glyphs.append(&mut shape_run( @@ -334,6 +336,7 @@ impl ShapeWord { } } +/// A shaped span (for bidirectional processing) pub struct ShapeSpan { pub rtl: bool, pub words: Vec, @@ -413,6 +416,7 @@ impl ShapeSpan { } } +/// A shaped line (or paragraph) pub struct ShapeLine { pub rtl: bool, pub spans: Vec, diff --git a/src/swash.rs b/src/swash.rs index 3d1f5b0..1b50dba 100644 --- a/src/swash.rs +++ b/src/swash.rs @@ -47,6 +47,7 @@ fn swash_image<'a>(font_system: &'a FontSystem<'a>, context: &mut ScaleContext, .render(&mut scaler, cache_key.glyph_id) } +/// Cache for rasterizing with the swash scaler pub struct SwashCache<'a> { font_system: &'a FontSystem<'a>, context: ScaleContext,