From 346af3f8b0baa418fd37b878bc2930ff0bd57cc0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 11 Sep 2023 02:47:24 +0200 Subject: Make `FontSystem` global and simplify `Paragraph` API --- core/src/renderer/null.rs | 34 ++-------- core/src/text.rs | 90 +++++++------------------ core/src/widget/text.rs | 23 +++---- graphics/Cargo.toml | 12 ++-- graphics/src/backend.rs | 4 -- graphics/src/renderer.rs | 35 ---------- graphics/src/text.rs | 47 ++++++------- graphics/src/text/paragraph.rs | 147 +++++++++++++++++++---------------------- renderer/src/lib.rs | 16 ----- tiny_skia/src/backend.rs | 5 -- tiny_skia/src/text.rs | 21 +++--- wgpu/src/backend.rs | 5 -- wgpu/src/text.rs | 17 +++-- widget/src/pick_list.rs | 22 +++--- widget/src/text_input.rs | 17 ++--- 15 files changed, 166 insertions(+), 329 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 55d58a59..0ffd3649 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -58,16 +58,6 @@ impl text::Renderer for Null { fn load_font(&mut self, _font: Cow<'static, [u8]>) {} - fn create_paragraph(&self, _text: Text<'_, Self::Font>) -> Self::Paragraph { - } - - fn resize_paragraph( - &self, - _paragraph: &mut Self::Paragraph, - _new_bounds: Size, - ) { - } - fn fill_paragraph( &mut self, _paragraph: &Self::Paragraph, @@ -88,24 +78,12 @@ impl text::Renderer for Null { impl text::Paragraph for () { type Font = Font; - fn content(&self) -> &str { - "" - } - - fn text_size(&self) -> Pixels { - Pixels(16.0) - } - - fn font(&self) -> Self::Font { - Font::default() - } + fn with_text(_text: Text<'_, Self::Font>) -> Self {} - fn line_height(&self) -> text::LineHeight { - text::LineHeight::default() - } + fn resize(&mut self, _new_bounds: Size) {} - fn shaping(&self) -> text::Shaping { - text::Shaping::default() + fn compare(&self, _text: Text<'_, Self::Font>) -> text::Difference { + text::Difference::None } fn horizontal_alignment(&self) -> alignment::Horizontal { @@ -120,10 +98,6 @@ impl text::Paragraph for () { None } - fn bounds(&self) -> Size { - Size::ZERO - } - fn min_bounds(&self) -> Size { Size::ZERO } diff --git a/core/src/text.rs b/core/src/text.rs index 0e3617b1..ff85696e 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -156,33 +156,6 @@ pub trait Renderer: crate::Renderer { /// Loads a [`Self::Font`] from its bytes. fn load_font(&mut self, font: Cow<'static, [u8]>); - /// Creates a new [`Paragraph`] laid out with the given [`Text`]. - fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph; - - /// Lays out the given [`Paragraph`] with some new boundaries. - fn resize_paragraph( - &self, - paragraph: &mut Self::Paragraph, - new_bounds: Size, - ); - - /// Updates a [`Paragraph`] to match the given [`Text`], if needed. - fn update_paragraph( - &self, - paragraph: &mut Self::Paragraph, - text: Text<'_, Self::Font>, - ) { - match compare(paragraph, text) { - Difference::None => {} - Difference::Bounds => { - self.resize_paragraph(paragraph, text.bounds); - } - Difference::Shape => { - *paragraph = self.create_paragraph(text); - } - } - } - /// Draws the given [`Paragraph`] at the given position and with the given /// [`Color`]. fn fill_paragraph( @@ -201,25 +174,21 @@ pub trait Renderer: crate::Renderer { color: Color, ); } + /// A text paragraph. -pub trait Paragraph: Default { +pub trait Paragraph: Sized + Default { /// The font of this [`Paragraph`]. - type Font; - - /// Returns the content of the [`Paragraph`]. - fn content(&self) -> &str; - - /// Returns the text size of the [`Paragraph`]. - fn text_size(&self) -> Pixels; + type Font: Copy + PartialEq; - /// Returns the [`LineHeight`] of the [`Paragraph`]. - fn line_height(&self) -> LineHeight; + /// Creates a new [`Paragraph`] laid out with the given [`Text`]. + fn with_text(text: Text<'_, Self::Font>) -> Self; - /// Returns the [`Self::Font`] of the [`Paragraph`]. - fn font(&self) -> Self::Font; + /// Lays out the [`Paragraph`] with some new boundaries. + fn resize(&mut self, new_bounds: Size); - /// Returns the [`Shaping`] strategy of the [`Paragraph`]. - fn shaping(&self) -> Shaping; + /// Compares the [`Paragraph`] with some desired [`Text`] and returns the + /// [`Difference`]. + fn compare(&self, text: Text<'_, Self::Font>) -> Difference; /// Returns the horizontal alignment of the [`Paragraph`]. fn horizontal_alignment(&self) -> alignment::Horizontal; @@ -227,9 +196,6 @@ pub trait Paragraph: Default { /// Returns the vertical alignment of the [`Paragraph`]. fn vertical_alignment(&self) -> alignment::Vertical; - /// Returns the boundaries of the [`Paragraph`]. - fn bounds(&self) -> Size; - /// Returns the minimum boundaries that can fit the contents of the /// [`Paragraph`]. fn min_bounds(&self) -> Size; @@ -241,6 +207,19 @@ pub trait Paragraph: Default { /// Returns the distance to the given grapheme index in the [`Paragraph`]. fn grapheme_position(&self, line: usize, index: usize) -> Option; + /// Updates the [`Paragraph`] to match the given [`Text`], if needed. + fn update(&mut self, text: Text<'_, Self::Font>) { + match self.compare(text) { + Difference::None => {} + Difference::Bounds => { + self.resize(text.bounds); + } + Difference::Shape => { + *self = Self::with_text(text); + } + } + } + /// Returns the minimum width that can fit the contents of the [`Paragraph`]. fn min_width(&self) -> f32 { self.min_bounds().width @@ -276,26 +255,3 @@ pub enum Difference { /// the text is necessary. Shape, } - -/// Compares a [`Paragraph`] with some desired [`Text`] and returns the -/// [`Difference`]. -pub fn compare( - paragraph: &impl Paragraph, - text: Text<'_, Font>, -) -> Difference { - if paragraph.content() != text.content - || paragraph.text_size() != text.size - || paragraph.line_height().to_absolute(text.size) - != text.line_height.to_absolute(text.size) - || paragraph.font() != text.font - || paragraph.shaping() != text.shaping - || paragraph.horizontal_alignment() != text.horizontal_alignment - || paragraph.vertical_alignment() != text.vertical_alignment - { - Difference::Shape - } else if paragraph.bounds() != text.bounds { - Difference::Bounds - } else { - Difference::None - } -} diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 53ed463e..c7c9f539 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -212,19 +212,16 @@ where let State(ref mut paragraph) = state; - renderer.update_paragraph( - paragraph, - text::Text { - content, - bounds, - size, - line_height, - font, - shaping, - horizontal_alignment, - vertical_alignment, - }, - ); + paragraph.update(text::Text { + content, + bounds, + size, + line_height, + font, + shaping, + horizontal_alignment, + vertical_alignment, + }); let size = limits.resolve(paragraph.min_bounds()); diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index ff698649..26bd1435 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -25,16 +25,15 @@ iced_core.workspace = true bitflags.workspace = true bytemuck.workspace = true +cosmic-text.workspace = true glam.workspace = true half.workspace = true log.workspace = true +once_cell.workspace = true raw-window-handle.workspace = true -thiserror.workspace = true -cosmic-text.workspace = true rustc-hash.workspace = true - -lyon_path.workspace = true -lyon_path.optional = true +thiserror.workspace = true +twox-hash.workspace = true image.workspace = true image.optional = true @@ -42,7 +41,8 @@ image.optional = true kamadak-exif.workspace = true kamadak-exif.optional = true -twox-hash.workspace = true +lyon_path.workspace = true +lyon_path.optional = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] twox-hash.workspace = true diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index c2ac82ba..10eb337f 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -2,7 +2,6 @@ use crate::core::image; use crate::core::svg; use crate::core::Size; -use crate::text; use std::borrow::Cow; @@ -18,9 +17,6 @@ pub trait Backend { pub trait Text { /// Loads a font from its bytes. fn load_font(&mut self, font: Cow<'static, [u8]>); - - /// Returns the [`cosmic_text::FontSystem`] of the [`Backend`]. - fn font_system(&self) -> &text::FontSystem; } /// A graphics backend that supports image rendering. diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index d4df29a5..c5033d36 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -158,41 +158,6 @@ where self.backend.load_font(bytes); } - fn create_paragraph(&self, text: Text<'_, Self::Font>) -> text::Paragraph { - text::Paragraph::with_text(text, self.backend.font_system()) - } - - fn update_paragraph( - &self, - paragraph: &mut Self::Paragraph, - text: Text<'_, Self::Font>, - ) { - let font_system = self.backend.font_system(); - - if paragraph.version() != font_system.version() { - // The font system has changed, paragraph fonts may be outdated - *paragraph = self.create_paragraph(text); - } else { - match core::text::compare(paragraph, text) { - core::text::Difference::None => {} - core::text::Difference::Bounds => { - self.resize_paragraph(paragraph, text.bounds); - } - core::text::Difference::Shape => { - *paragraph = self.create_paragraph(text); - } - } - } - } - - fn resize_paragraph( - &self, - paragraph: &mut Self::Paragraph, - new_bounds: Size, - ) { - paragraph.resize(new_bounds, self.backend.font_system()); - } - fn fill_paragraph( &mut self, paragraph: &Self::Paragraph, diff --git a/graphics/src/text.rs b/graphics/src/text.rs index bc06aa3c..f5ccaf52 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -10,40 +10,39 @@ use crate::core::font::{self, Font}; use crate::core::text::Shaping; use crate::core::Size; +use once_cell::sync::OnceCell; use std::borrow::Cow; -use std::sync::{self, Arc, RwLock}; +use std::sync::{Arc, RwLock}; -#[allow(missing_debug_implementations)] -pub struct FontSystem { - raw: RwLock, - version: Version, -} +pub fn font_system() -> &'static RwLock { + static FONT_SYSTEM: OnceCell> = OnceCell::new(); -impl FontSystem { - pub fn new() -> Self { - FontSystem { - raw: RwLock::new(cosmic_text::FontSystem::new_with_fonts( + FONT_SYSTEM.get_or_init(|| { + RwLock::new(FontSystem { + raw: cosmic_text::FontSystem::new_with_fonts( [cosmic_text::fontdb::Source::Binary(Arc::new( include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), ))] .into_iter(), - )), + ), version: Version::default(), - } - } + }) + }) +} - pub fn get_mut(&mut self) -> &mut cosmic_text::FontSystem { - self.raw.get_mut().expect("Lock font system") - } +#[allow(missing_debug_implementations)] +pub struct FontSystem { + raw: cosmic_text::FontSystem, + version: Version, +} - pub fn write( - &self, - ) -> (sync::RwLockWriteGuard<'_, cosmic_text::FontSystem>, Version) { - (self.raw.write().expect("Write font system"), self.version) +impl FontSystem { + pub fn raw(&mut self) -> &mut cosmic_text::FontSystem { + &mut self.raw } pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - let _ = self.get_mut().db_mut().load_font_source( + let _ = self.raw.db_mut().load_font_source( cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), ); @@ -58,12 +57,6 @@ impl FontSystem { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Version(u32); -impl Default for FontSystem { - fn default() -> Self { - Self::new() - } -} - pub fn measure(buffer: &cosmic_text::Buffer) -> Size { let (width, total_lines) = buffer .layout_runs() diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index e4350cff..d0396e8e 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -2,7 +2,7 @@ use crate::core; use crate::core::alignment; use crate::core::text::{Hit, LineHeight, Shaping, Text}; use crate::core::{Font, Pixels, Point, Size}; -use crate::text::{self, FontSystem}; +use crate::text; use std::fmt; use std::sync::{self, Arc}; @@ -27,13 +27,39 @@ impl Paragraph { Self::default() } - pub fn with_text(text: Text<'_, Font>, font_system: &FontSystem) -> Self { + pub fn buffer(&self) -> &cosmic_text::Buffer { + &self.internal().buffer + } + + pub fn downgrade(&self) -> Weak { + let paragraph = self.internal(); + + Weak { + raw: Arc::downgrade(paragraph), + min_bounds: paragraph.min_bounds, + horizontal_alignment: paragraph.horizontal_alignment, + vertical_alignment: paragraph.vertical_alignment, + } + } + + fn internal(&self) -> &Arc { + self.0 + .as_ref() + .expect("paragraph should always be initialized") + } +} + +impl core::text::Paragraph for Paragraph { + type Font = Font; + + fn with_text(text: Text<'_, Font>) -> Self { log::trace!("Allocating paragraph: {}", text.content); - let (mut font_system, version) = font_system.write(); + let mut font_system = + text::font_system().write().expect("Write font system"); let mut buffer = cosmic_text::Buffer::new( - &mut font_system, + font_system.raw(), cosmic_text::Metrics::new( text.size.into(), text.line_height.to_absolute(text.size).into(), @@ -41,13 +67,13 @@ impl Paragraph { ); buffer.set_size( - &mut font_system, + font_system.raw(), text.bounds.width, text.bounds.height, ); buffer.set_text( - &mut font_system, + font_system.raw(), text.content, text::to_attributes(text.font), text::to_shaping(text.shaping), @@ -64,30 +90,11 @@ impl Paragraph { shaping: text.shaping, bounds: text.bounds, min_bounds, - version, + version: font_system.version(), }))) } - pub fn buffer(&self) -> &cosmic_text::Buffer { - &self.internal().buffer - } - - pub fn version(&self) -> text::Version { - self.internal().version - } - - pub fn downgrade(&self) -> Weak { - let paragraph = self.internal(); - - Weak { - raw: Arc::downgrade(paragraph), - min_bounds: paragraph.min_bounds, - horizontal_alignment: paragraph.horizontal_alignment, - vertical_alignment: paragraph.vertical_alignment, - } - } - - pub fn resize(&mut self, new_bounds: Size, font_system: &FontSystem) { + fn resize(&mut self, new_bounds: Size) { let paragraph = self .0 .take() @@ -95,10 +102,11 @@ impl Paragraph { match Arc::try_unwrap(paragraph) { Ok(mut internal) => { - let (mut font_system, _) = font_system.write(); + let mut font_system = + text::font_system().write().expect("Write font system"); internal.buffer.set_size( - &mut font_system, + font_system.raw(), new_bounds.width, new_bounds.height, ); @@ -113,55 +121,42 @@ impl Paragraph { // If there is a strong reference somewhere, we recompute the // buffer from scratch - *self = Self::with_text( - Text { - content: &internal.content, - bounds: internal.bounds, - size: Pixels(metrics.font_size), - line_height: LineHeight::Absolute(Pixels( - metrics.line_height, - )), - font: internal.font, - horizontal_alignment: internal.horizontal_alignment, - vertical_alignment: internal.vertical_alignment, - shaping: internal.shaping, - }, - font_system, - ); + *self = Self::with_text(Text { + content: &internal.content, + bounds: internal.bounds, + size: Pixels(metrics.font_size), + line_height: LineHeight::Absolute(Pixels( + metrics.line_height, + )), + font: internal.font, + horizontal_alignment: internal.horizontal_alignment, + vertical_alignment: internal.vertical_alignment, + shaping: internal.shaping, + }); } } } - fn internal(&self) -> &Arc { - self.0 - .as_ref() - .expect("paragraph should always be initialized") - } -} - -impl core::text::Paragraph for Paragraph { - type Font = Font; - - fn content(&self) -> &str { - &self.internal().content - } - - fn text_size(&self) -> Pixels { - Pixels(self.internal().buffer.metrics().font_size) - } - - fn line_height(&self) -> LineHeight { - LineHeight::Absolute(Pixels( - self.internal().buffer.metrics().line_height, - )) - } - - fn font(&self) -> Font { - self.internal().font - } - - fn shaping(&self) -> Shaping { - self.internal().shaping + fn compare(&self, text: Text<'_, Font>) -> core::text::Difference { + let font_system = text::font_system().read().expect("Read font system"); + let paragraph = self.internal(); + let metrics = paragraph.buffer.metrics(); + + if paragraph.version != font_system.version + || paragraph.content != text.content + || metrics.font_size != text.size.0 + || metrics.line_height != text.line_height.to_absolute(text.size).0 + || paragraph.font != text.font + || paragraph.shaping != text.shaping + || paragraph.horizontal_alignment != text.horizontal_alignment + || paragraph.vertical_alignment != text.vertical_alignment + { + core::text::Difference::Shape + } else if paragraph.bounds != text.bounds { + core::text::Difference::Bounds + } else { + core::text::Difference::None + } } fn horizontal_alignment(&self) -> alignment::Horizontal { @@ -172,10 +167,6 @@ impl core::text::Paragraph for Paragraph { self.internal().vertical_alignment } - fn bounds(&self) -> Size { - self.internal().bounds - } - fn min_bounds(&self) -> Size { self.internal().min_bounds } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 8bdf231d..73e56890 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -173,22 +173,6 @@ impl text::Renderer for Renderer { delegate!(self, renderer, renderer.default_size()) } - fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph { - delegate!(self, renderer, renderer.create_paragraph(text)) - } - - fn resize_paragraph( - &self, - paragraph: &mut Self::Paragraph, - new_bounds: Size, - ) { - delegate!( - self, - renderer, - renderer.resize_paragraph(paragraph, new_bounds) - ); - } - fn load_font(&mut self, bytes: Cow<'static, [u8]>) { delegate!(self, renderer, renderer.load_font(bytes)); } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index c721d96e..72184c8a 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,6 +1,5 @@ use crate::core::{Background, Color, Gradient, Rectangle, Vector}; use crate::graphics::backend; -use crate::graphics::text; use crate::graphics::{Damage, Viewport}; use crate::primitive::{self, Primitive}; @@ -805,10 +804,6 @@ impl iced_graphics::Backend for Backend { } impl backend::Text for Backend { - fn font_system(&self) -> &text::FontSystem { - self.text_pipeline.font_system() - } - fn load_font(&mut self, font: Cow<'static, [u8]>) { self.text_pipeline.load_font(font); } diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index cb3ef54c..4f6e3941 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -2,8 +2,8 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; use crate::core::{Color, Font, Pixels, Point, Rectangle}; use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::font_system; use crate::graphics::text::paragraph; -use crate::graphics::text::FontSystem; use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; @@ -12,7 +12,6 @@ use std::collections::hash_map; #[allow(missing_debug_implementations)] pub struct Pipeline { - font_system: FontSystem, glyph_cache: GlyphCache, cache: RefCell, } @@ -20,18 +19,16 @@ pub struct Pipeline { impl Pipeline { pub fn new() -> Self { Pipeline { - font_system: FontSystem::new(), glyph_cache: GlyphCache::new(), cache: RefCell::new(Cache::new()), } } - pub fn font_system(&self) -> &FontSystem { - &self.font_system - } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - self.font_system.load_font(bytes); + font_system() + .write() + .expect("Write font system") + .load_font(bytes); self.cache = RefCell::new(Cache::new()); } @@ -51,8 +48,10 @@ impl Pipeline { return; }; + let mut font_system = font_system().write().expect("Write font system"); + draw( - self.font_system.get_mut(), + font_system.raw(), &mut self.glyph_cache, paragraph.buffer(), Rectangle::new(position, paragraph.min_bounds()), @@ -82,7 +81,9 @@ impl Pipeline { ) { let line_height = f32::from(line_height.to_absolute(size)); - let font_system = self.font_system.get_mut(); + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + let key = cache::Key { bounds: bounds.size(), content, diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 65c63f19..3d1755e1 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -1,5 +1,4 @@ use crate::core::{Color, Size}; -use crate::graphics; use crate::graphics::backend; use crate::graphics::color; use crate::graphics::{Transformation, Viewport}; @@ -310,10 +309,6 @@ impl crate::graphics::Backend for Backend { } impl backend::Text for Backend { - fn font_system(&self) -> &graphics::text::FontSystem { - self.text_pipeline.font_system() - } - fn load_font(&mut self, font: Cow<'static, [u8]>) { self.text_pipeline.load_font(font); } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index bd4f3e06..5c9f4d7e 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,7 +2,7 @@ use crate::core::alignment; use crate::core::{Rectangle, Size}; use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; -use crate::graphics::text::{FontSystem, Paragraph}; +use crate::graphics::text::{font_system, Paragraph}; use crate::layer::Text; use std::borrow::Cow; @@ -10,7 +10,6 @@ use std::cell::RefCell; #[allow(missing_debug_implementations)] pub struct Pipeline { - font_system: FontSystem, renderers: Vec, atlas: glyphon::TextAtlas, prepare_layer: usize, @@ -24,7 +23,6 @@ impl Pipeline { format: wgpu::TextureFormat, ) -> Self { Pipeline { - font_system: FontSystem::new(), renderers: Vec::new(), atlas: glyphon::TextAtlas::with_color_mode( device, @@ -41,12 +39,11 @@ impl Pipeline { } } - pub fn font_system(&self) -> &FontSystem { - &self.font_system - } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - self.font_system.load_font(bytes); + font_system() + .write() + .expect("Write font system") + .load_font(bytes); self.cache = RefCell::new(Cache::new()); } @@ -69,7 +66,9 @@ impl Pipeline { )); } - let font_system = self.font_system.get_mut(); + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + let renderer = &mut self.renderers[self.prepare_layer]; let cache = self.cache.get_mut(); diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 056a5e65..4b89d6ff 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -415,23 +415,17 @@ where for (option, paragraph) in options.iter().zip(state.options.iter_mut()) { let label = option.to_string(); - renderer.update_paragraph( - paragraph, - Text { - content: &label, - ..option_text - }, - ); + paragraph.update(Text { + content: &label, + ..option_text + }); } if let Some(placeholder) = placeholder { - renderer.update_paragraph( - &mut state.placeholder, - Text { - content: placeholder, - ..option_text - }, - ); + state.placeholder.update(Text { + content: placeholder, + ..option_text + }); } let max_width = match width { diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 7d5ae806..f9a2d419 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -523,18 +523,15 @@ where shaping: text::Shaping::Advanced, }; - renderer.update_paragraph(&mut state.placeholder, placeholder_text); + state.placeholder.update(placeholder_text); let secure_value = is_secure.then(|| value.secure()); let value = secure_value.as_ref().unwrap_or(value); - renderer.update_paragraph( - &mut state.value, - Text { - content: &value.to_string(), - ..placeholder_text - }, - ); + state.value.update(Text { + content: &value.to_string(), + ..placeholder_text + }); if let Some(icon) = icon { let icon_text = Text { @@ -548,7 +545,7 @@ where shaping: text::Shaping::Advanced, }; - renderer.update_paragraph(&mut state.icon, icon_text); + state.icon.update(icon_text); let icon_width = state.icon.min_width(); @@ -1461,7 +1458,7 @@ fn replace_paragraph( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - state.value = renderer.create_paragraph(Text { + state.value = Renderer::Paragraph::with_text(Text { font, line_height, content: &value.to_string(), -- cgit From 6448429103c9c82b90040ac5a5a097bdded23f82 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 12 Sep 2023 14:51:00 +0200 Subject: Draft `Editor` API and `TextEditor` widget --- core/src/layout/limits.rs | 2 +- core/src/lib.rs | 2 +- core/src/renderer/null.rs | 38 ++++ core/src/text.rs | 123 ++++-------- core/src/text/editor.rs | 68 +++++++ core/src/text/paragraph.rs | 59 ++++++ examples/editor/Cargo.toml | 10 + examples/editor/src/main.rs | 49 +++++ graphics/src/damage.rs | 7 + graphics/src/primitive.rs | 10 + graphics/src/renderer.rs | 14 ++ graphics/src/text.rs | 2 + graphics/src/text/editor.rs | 327 +++++++++++++++++++++++++++++++ renderer/src/lib.rs | 19 +- style/src/lib.rs | 1 + style/src/text_editor.rs | 47 +++++ style/src/theme.rs | 113 +++++++++++ tiny_skia/src/backend.rs | 25 +++ tiny_skia/src/text.rs | 32 ++++ wgpu/src/layer.rs | 15 +- wgpu/src/layer/text.rs | 8 +- wgpu/src/text.rs | 28 ++- widget/src/helpers.rs | 15 ++ widget/src/lib.rs | 5 +- widget/src/text_editor.rs | 457 ++++++++++++++++++++++++++++++++++++++++++++ 25 files changed, 1384 insertions(+), 92 deletions(-) create mode 100644 core/src/text/editor.rs create mode 100644 core/src/text/paragraph.rs create mode 100644 examples/editor/Cargo.toml create mode 100644 examples/editor/src/main.rs create mode 100644 graphics/src/text/editor.rs create mode 100644 style/src/text_editor.rs create mode 100644 widget/src/text_editor.rs diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs index 5d3c1556..39a3d98b 100644 --- a/core/src/layout/limits.rs +++ b/core/src/layout/limits.rs @@ -2,7 +2,7 @@ use crate::{Length, Padding, Size}; /// A set of size constraints for layouting. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Limits { min: Size, max: Size, diff --git a/core/src/lib.rs b/core/src/lib.rs index 1bfba7bd..13a9f06b 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -12,7 +12,7 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![deny( missing_debug_implementations, - missing_docs, + // missing_docs, unused_results, clippy::extra_unused_lifetimes, clippy::from_over_into, diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 0ffd3649..adf75969 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -43,6 +43,7 @@ impl Renderer for Null { impl text::Renderer for Null { type Font = Font; type Paragraph = (); + type Editor = (); const ICON_FONT: Font = Font::DEFAULT; const CHECKMARK_ICON: char = '0'; @@ -66,6 +67,14 @@ impl text::Renderer for Null { ) { } + fn fill_editor( + &mut self, + _editor: &Self::Editor, + _position: Point, + _color: Color, + ) { + } + fn fill_text( &mut self, _paragraph: Text<'_, Self::Font>, @@ -106,3 +115,32 @@ impl text::Paragraph for () { None } } + +impl text::Editor for () { + type Font = Font; + + fn with_text(_text: &str) -> Self {} + + fn cursor(&self) -> text::editor::Cursor { + text::editor::Cursor::Caret(Point::ORIGIN) + } + + fn perform(&mut self, _action: text::editor::Action) {} + + fn bounds(&self) -> Size { + Size::ZERO + } + + fn min_bounds(&self) -> Size { + Size::ZERO + } + + fn update( + &mut self, + _new_bounds: Size, + _new_font: Self::Font, + _new_size: Pixels, + _new_line_height: text::LineHeight, + ) { + } +} diff --git a/core/src/text.rs b/core/src/text.rs index ff85696e..5aacbcc5 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -1,4 +1,11 @@ //! Draw and interact with text. +mod paragraph; + +pub mod editor; + +pub use editor::Editor; +pub use paragraph::Paragraph; + use crate::alignment; use crate::{Color, Pixels, Point, Size}; @@ -126,6 +133,31 @@ impl Hit { } } +/// The difference detected in some text. +/// +/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some +/// [`Text`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Difference { + /// No difference. + /// + /// The text can be reused as it is! + None, + + /// A bounds difference. + /// + /// This normally means a relayout is necessary, but the shape of the text can + /// be reused. + Bounds, + + /// A shape difference. + /// + /// The contents, alignment, sizes, fonts, or any other essential attributes + /// of the shape of the text have changed. A complete reshape and relayout of + /// the text is necessary. + Shape, +} + /// A renderer capable of measuring and drawing [`Text`]. pub trait Renderer: crate::Renderer { /// The font type used. @@ -134,6 +166,9 @@ pub trait Renderer: crate::Renderer { /// The [`Paragraph`] of this [`Renderer`]. type Paragraph: Paragraph + 'static; + /// The [`Editor`] of this [`Renderer`]. + type Editor: Editor + 'static; + /// The icon font of the backend. const ICON_FONT: Self::Font; @@ -165,6 +200,13 @@ pub trait Renderer: crate::Renderer { color: Color, ); + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + ); + /// Draws the given [`Text`] at the given position and with the given /// [`Color`]. fn fill_text( @@ -174,84 +216,3 @@ pub trait Renderer: crate::Renderer { color: Color, ); } - -/// A text paragraph. -pub trait Paragraph: Sized + Default { - /// The font of this [`Paragraph`]. - type Font: Copy + PartialEq; - - /// Creates a new [`Paragraph`] laid out with the given [`Text`]. - fn with_text(text: Text<'_, Self::Font>) -> Self; - - /// Lays out the [`Paragraph`] with some new boundaries. - fn resize(&mut self, new_bounds: Size); - - /// Compares the [`Paragraph`] with some desired [`Text`] and returns the - /// [`Difference`]. - fn compare(&self, text: Text<'_, Self::Font>) -> Difference; - - /// Returns the horizontal alignment of the [`Paragraph`]. - fn horizontal_alignment(&self) -> alignment::Horizontal; - - /// Returns the vertical alignment of the [`Paragraph`]. - fn vertical_alignment(&self) -> alignment::Vertical; - - /// Returns the minimum boundaries that can fit the contents of the - /// [`Paragraph`]. - fn min_bounds(&self) -> Size; - - /// Tests whether the provided point is within the boundaries of the - /// [`Paragraph`], returning information about the nearest character. - fn hit_test(&self, point: Point) -> Option; - - /// Returns the distance to the given grapheme index in the [`Paragraph`]. - fn grapheme_position(&self, line: usize, index: usize) -> Option; - - /// Updates the [`Paragraph`] to match the given [`Text`], if needed. - fn update(&mut self, text: Text<'_, Self::Font>) { - match self.compare(text) { - Difference::None => {} - Difference::Bounds => { - self.resize(text.bounds); - } - Difference::Shape => { - *self = Self::with_text(text); - } - } - } - - /// Returns the minimum width that can fit the contents of the [`Paragraph`]. - fn min_width(&self) -> f32 { - self.min_bounds().width - } - - /// Returns the minimum height that can fit the contents of the [`Paragraph`]. - fn min_height(&self) -> f32 { - self.min_bounds().height - } -} - -/// The difference detected in some text. -/// -/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some -/// [`Text`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Difference { - /// No difference. - /// - /// The text can be reused as it is! - None, - - /// A bounds difference. - /// - /// This normally means a relayout is necessary, but the shape of the text can - /// be reused. - Bounds, - - /// A shape difference. - /// - /// The contents, alignment, sizes, fonts, or any other essential attributes - /// of the shape of the text have changed. A complete reshape and relayout of - /// the text is necessary. - Shape, -} diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs new file mode 100644 index 00000000..a4fd0ec1 --- /dev/null +++ b/core/src/text/editor.rs @@ -0,0 +1,68 @@ +use crate::text::LineHeight; +use crate::{Pixels, Point, Rectangle, Size}; + +pub trait Editor: Sized + Default { + type Font: Copy + PartialEq + Default; + + /// Creates a new [`Editor`] laid out with the given text. + fn with_text(text: &str) -> Self; + + fn cursor(&self) -> Cursor; + + fn perform(&mut self, action: Action); + + /// Returns the current boundaries of the [`Editor`]. + fn bounds(&self) -> Size; + + /// Returns the minimum boundaries that can fit the contents of the + /// [`Editor`]. + fn min_bounds(&self) -> Size; + + /// Updates the [`Editor`] with some new attributes. + fn update( + &mut self, + new_bounds: Size, + new_font: Self::Font, + new_size: Pixels, + new_line_height: LineHeight, + ); + + /// Returns the minimum width that can fit the contents of the [`Editor`]. + fn min_width(&self) -> f32 { + self.min_bounds().width + } + + /// Returns the minimum height that can fit the contents of the [`Editor`]. + fn min_height(&self) -> f32 { + self.min_bounds().height + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Action { + MoveLeft, + MoveRight, + MoveUp, + MoveDown, + MoveLeftWord, + MoveRightWord, + MoveHome, + MoveEnd, + SelectWord, + SelectLine, + Insert(char), + Backspace, + Delete, + Click(Point), + Drag(Point), +} + +/// The cursor of an [`Editor`]. +#[derive(Debug, Clone)] +pub enum Cursor { + /// Cursor without a selection + Caret(Point), + + /// Cursor selecting a range of text + Selection(Vec), +} diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs new file mode 100644 index 00000000..de1fb74d --- /dev/null +++ b/core/src/text/paragraph.rs @@ -0,0 +1,59 @@ +use crate::alignment; +use crate::text::{Difference, Hit, Text}; +use crate::{Point, Size}; + +/// A text paragraph. +pub trait Paragraph: Sized + Default { + /// The font of this [`Paragraph`]. + type Font: Copy + PartialEq; + + /// Creates a new [`Paragraph`] laid out with the given [`Text`]. + fn with_text(text: Text<'_, Self::Font>) -> Self; + + /// Lays out the [`Paragraph`] with some new boundaries. + fn resize(&mut self, new_bounds: Size); + + /// Compares the [`Paragraph`] with some desired [`Text`] and returns the + /// [`Difference`]. + fn compare(&self, text: Text<'_, Self::Font>) -> Difference; + + /// Returns the horizontal alignment of the [`Paragraph`]. + fn horizontal_alignment(&self) -> alignment::Horizontal; + + /// Returns the vertical alignment of the [`Paragraph`]. + fn vertical_alignment(&self) -> alignment::Vertical; + + /// Returns the minimum boundaries that can fit the contents of the + /// [`Paragraph`]. + fn min_bounds(&self) -> Size; + + /// Tests whether the provided point is within the boundaries of the + /// [`Paragraph`], returning information about the nearest character. + fn hit_test(&self, point: Point) -> Option; + + /// Returns the distance to the given grapheme index in the [`Paragraph`]. + fn grapheme_position(&self, line: usize, index: usize) -> Option; + + /// Updates the [`Paragraph`] to match the given [`Text`], if needed. + fn update(&mut self, text: Text<'_, Self::Font>) { + match self.compare(text) { + Difference::None => {} + Difference::Bounds => { + self.resize(text.bounds); + } + Difference::Shape => { + *self = Self::with_text(text); + } + } + } + + /// Returns the minimum width that can fit the contents of the [`Paragraph`]. + fn min_width(&self) -> f32 { + self.min_bounds().width + } + + /// Returns the minimum height that can fit the contents of the [`Paragraph`]. + fn min_height(&self) -> f32 { + self.min_bounds().height + } +} diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml new file mode 100644 index 00000000..528cf23c --- /dev/null +++ b/examples/editor/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "editor" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["debug"] \ No newline at end of file diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs new file mode 100644 index 00000000..50989ac5 --- /dev/null +++ b/examples/editor/src/main.rs @@ -0,0 +1,49 @@ +use iced::widget::{container, text_editor}; +use iced::{Element, Font, Sandbox, Settings}; + +pub fn main() -> iced::Result { + Editor::run(Settings::default()) +} + +struct Editor { + content: text_editor::Content, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Edit(text_editor::Action), +} + +impl Sandbox for Editor { + type Message = Message; + + fn new() -> Self { + Self { + content: text_editor::Content::with(include_str!( + "../../../README.md" + )), + } + } + + fn title(&self) -> String { + String::from("Editor - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::Edit(action) => { + self.content.edit(action); + } + } + } + + fn view(&self) -> Element { + container( + text_editor(&self.content) + .on_edit(Message::Edit) + .font(Font::with_name("Hasklug Nerd Font Mono")), + ) + .padding(20) + .into() + } +} diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index 3276c2d4..595cc274 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -66,6 +66,13 @@ impl Damage for Primitive { bounds.expand(1.5) } + Self::Editor { + editor, position, .. + } => { + let bounds = Rectangle::new(*position, editor.bounds); + + bounds.expand(1.5) + } Self::Quad { bounds, .. } | Self::Image { bounds, .. } | Self::Svg { bounds, .. } => bounds.expand(1.0), diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 8a97e6e7..ce0b734b 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -4,6 +4,7 @@ use crate::core::image; use crate::core::svg; use crate::core::text; use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector}; +use crate::text::editor; use crate::text::paragraph; use std::sync::Arc; @@ -41,6 +42,15 @@ pub enum Primitive { /// The color of the paragraph. color: Color, }, + /// An editor primitive + Editor { + /// The [`editor::Weak`] reference. + editor: editor::Weak, + /// The position of the paragraph. + position: Point, + /// The color of the paragraph. + color: Color, + }, /// A quad primitive Quad { /// The bounds of the quad diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index c5033d36..9b699183 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -141,6 +141,7 @@ where { type Font = Font; type Paragraph = text::Paragraph; + type Editor = text::Editor; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; @@ -171,6 +172,19 @@ where }); } + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + ) { + self.primitives.push(Primitive::Editor { + editor: editor.downgrade(), + position, + color, + }); + } + fn fill_text( &mut self, text: Text<'_, Self::Font>, diff --git a/graphics/src/text.rs b/graphics/src/text.rs index f5ccaf52..280e4f01 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -1,7 +1,9 @@ pub mod cache; +pub mod editor; pub mod paragraph; pub use cache::Cache; +pub use editor::Editor; pub use paragraph::Paragraph; pub use cosmic_text; diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs new file mode 100644 index 00000000..53f63fea --- /dev/null +++ b/graphics/src/text/editor.rs @@ -0,0 +1,327 @@ +use crate::core::text::editor::{self, Action, Cursor}; +use crate::core::text::LineHeight; +use crate::core::{Font, Pixels, Point, Size}; +use crate::text; + +use cosmic_text::Edit; + +use std::fmt; +use std::sync::{self, Arc}; + +#[derive(Debug, PartialEq)] +pub struct Editor(Option>); + +struct Internal { + editor: cosmic_text::Editor, + font: Font, + bounds: Size, + min_bounds: Size, + version: text::Version, +} + +impl Editor { + pub fn new() -> Self { + Self::default() + } + + pub fn buffer(&self) -> &cosmic_text::Buffer { + &self.internal().editor.buffer() + } + + pub fn downgrade(&self) -> Weak { + let editor = self.internal(); + + Weak { + raw: Arc::downgrade(editor), + bounds: editor.bounds, + } + } + + fn internal(&self) -> &Arc { + self.0 + .as_ref() + .expect("editor should always be initialized") + } +} + +impl editor::Editor for Editor { + type Font = Font; + + fn with_text(text: &str) -> Self { + let mut buffer = cosmic_text::Buffer::new_empty(cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }); + + buffer.set_text( + text::font_system() + .write() + .expect("Write font system") + .raw(), + text, + cosmic_text::Attrs::new(), + cosmic_text::Shaping::Advanced, + ); + + Editor(Some(Arc::new(Internal { + editor: cosmic_text::Editor::new(buffer), + ..Default::default() + }))) + } + + fn cursor(&self) -> editor::Cursor { + let internal = self.internal(); + + match internal.editor.select_opt() { + Some(selection) => { + // TODO + Cursor::Selection(vec![]) + } + None => { + let cursor = internal.editor.cursor(); + let buffer = internal.editor.buffer(); + + let lines_before_cursor: usize = buffer + .lines + .iter() + .take(cursor.line) + .map(|line| { + line.layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() + }) + .sum(); + + let line = buffer + .lines + .get(cursor.line) + .expect("Cursor line should be present"); + + let layout = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached"); + + let mut lines = layout.iter().enumerate(); + + let (subline, offset) = lines + .find_map(|(i, line)| { + let start = line + .glyphs + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let is_cursor_after_start = start <= cursor.index; + + let is_cursor_before_end = match cursor.affinity { + cosmic_text::Affinity::Before => { + cursor.index <= end + } + cosmic_text::Affinity::After => cursor.index < end, + }; + + if is_cursor_after_start && is_cursor_before_end { + let offset = line + .glyphs + .iter() + .take_while(|glyph| cursor.index > glyph.start) + .map(|glyph| glyph.w) + .sum(); + + Some((i, offset)) + } else { + None + } + }) + .unwrap_or((0, 0.0)); + + let line_height = buffer.metrics().line_height; + + let scroll_offset = buffer.scroll() as f32 * line_height; + + Cursor::Caret(Point::new( + offset, + (lines_before_cursor + subline) as f32 * line_height + - scroll_offset, + )) + } + } + } + + fn perform(&mut self, action: Action) { + let mut font_system = + text::font_system().write().expect("Write font system"); + + let editor = + self.0.take().expect("Editor should always be initialized"); + + // TODO: Handle multiple strong references somehow + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let editor = &mut internal.editor; + + let mut act = |action| editor.action(font_system.raw(), action); + + match action { + Action::MoveLeft => act(cosmic_text::Action::Left), + Action::MoveRight => act(cosmic_text::Action::Right), + Action::MoveUp => act(cosmic_text::Action::Up), + Action::MoveDown => act(cosmic_text::Action::Down), + Action::Insert(c) => act(cosmic_text::Action::Insert(c)), + Action::Backspace => act(cosmic_text::Action::Backspace), + Action::Delete => act(cosmic_text::Action::Delete), + Action::Click(position) => act(cosmic_text::Action::Click { + x: position.x as i32, + y: position.y as i32, + }), + Action::Drag(position) => act(cosmic_text::Action::Drag { + x: position.x as i32, + y: position.y as i32, + }), + _ => todo!(), + } + + editor.shape_as_needed(font_system.raw()); + + self.0 = Some(Arc::new(internal)); + } + + fn bounds(&self) -> Size { + self.internal().bounds + } + + fn min_bounds(&self) -> Size { + self.internal().min_bounds + } + + fn update( + &mut self, + new_bounds: Size, + new_font: Font, + new_size: Pixels, + new_line_height: LineHeight, + ) { + let editor = + self.0.take().expect("editor should always be initialized"); + + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut changed = false; + + if new_font != internal.font { + for line in internal.editor.buffer_mut().lines.iter_mut() { + let _ = line.set_attrs_list(cosmic_text::AttrsList::new( + text::to_attributes(new_font), + )); + } + + changed = true; + } + + let metrics = internal.editor.buffer().metrics(); + let new_line_height = new_line_height.to_absolute(new_size); + + if new_size.0 != metrics.font_size + || new_line_height.0 != metrics.line_height + { + internal.editor.buffer_mut().set_metrics( + font_system.raw(), + cosmic_text::Metrics::new(new_size.0, new_line_height.0), + ); + + changed = true; + } + + if new_bounds != internal.bounds { + internal.editor.buffer_mut().set_size( + font_system.raw(), + new_bounds.width, + new_bounds.height, + ); + + internal.bounds = new_bounds; + changed = true; + } + + if changed { + internal.min_bounds = text::measure(&internal.editor.buffer()); + } + + self.0 = Some(Arc::new(internal)); + } +} + +impl Default for Editor { + fn default() -> Self { + Self(Some(Arc::new(Internal::default()))) + } +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + self.font == other.font + && self.bounds == other.bounds + && self.min_bounds == other.min_bounds + && self.editor.buffer().metrics() == other.editor.buffer().metrics() + } +} + +impl Default for Internal { + fn default() -> Self { + Self { + editor: cosmic_text::Editor::new(cosmic_text::Buffer::new_empty( + cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }, + )), + font: Font::default(), + bounds: Size::ZERO, + min_bounds: Size::ZERO, + version: text::Version::default(), + } + } +} + +impl fmt::Debug for Internal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Internal") + .field("font", &self.font) + .field("bounds", &self.bounds) + .field("min_bounds", &self.min_bounds) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct Weak { + raw: sync::Weak, + pub bounds: Size, +} + +impl Weak { + pub fn upgrade(&self) -> Option { + self.raw.upgrade().map(Some).map(Editor) + } +} + +impl PartialEq for Weak { + fn eq(&self, other: &Self) -> bool { + match (self.raw.upgrade(), other.raw.upgrade()) { + (Some(p1), Some(p2)) => p1 == p2, + _ => false, + } + } +} diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 73e56890..6f044af6 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -32,6 +32,7 @@ use crate::core::text::{self, Text}; use crate::core::{ Background, Color, Font, Pixels, Point, Rectangle, Size, Vector, }; +use crate::graphics::text::Editor; use crate::graphics::text::Paragraph; use crate::graphics::Mesh; @@ -159,6 +160,7 @@ impl core::Renderer for Renderer { impl text::Renderer for Renderer { type Font = Font; type Paragraph = Paragraph; + type Editor = Editor; const ICON_FONT: Font = iced_tiny_skia::Renderer::::ICON_FONT; const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::::CHECKMARK_ICON; @@ -179,14 +181,27 @@ impl text::Renderer for Renderer { fn fill_paragraph( &mut self, - text: &Self::Paragraph, + paragraph: &Self::Paragraph, position: Point, color: Color, ) { delegate!( self, renderer, - renderer.fill_paragraph(text, position, color) + renderer.fill_paragraph(paragraph, position, color) + ); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + ) { + delegate!( + self, + renderer, + renderer.fill_editor(editor, position, color) ); } diff --git a/style/src/lib.rs b/style/src/lib.rs index 0c555ed8..7a97ac77 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -35,6 +35,7 @@ pub mod rule; pub mod scrollable; pub mod slider; pub mod svg; +pub mod text_editor; pub mod text_input; pub mod theme; pub mod toggler; diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs new file mode 100644 index 00000000..45c9bad8 --- /dev/null +++ b/style/src/text_editor.rs @@ -0,0 +1,47 @@ +//! Change the appearance of a text editor. +use iced_core::{Background, BorderRadius, Color}; + +/// The appearance of a text input. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the text input. + pub background: Background, + /// The border radius of the text input. + pub border_radius: BorderRadius, + /// The border width of the text input. + pub border_width: f32, + /// The border [`Color`] of the text input. + pub border_color: Color, +} + +/// A set of rules that dictate the style of a text input. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the style of an active text input. + fn active(&self, style: &Self::Style) -> Appearance; + + /// Produces the style of a focused text input. + fn focused(&self, style: &Self::Style) -> Appearance; + + /// Produces the [`Color`] of the placeholder of a text input. + fn placeholder_color(&self, style: &Self::Style) -> Color; + + /// Produces the [`Color`] of the value of a text input. + fn value_color(&self, style: &Self::Style) -> Color; + + /// Produces the [`Color`] of the value of a disabled text input. + fn disabled_color(&self, style: &Self::Style) -> Color; + + /// Produces the [`Color`] of the selection of a text input. + fn selection_color(&self, style: &Self::Style) -> Color; + + /// Produces the style of an hovered text input. + fn hovered(&self, style: &Self::Style) -> Appearance { + self.focused(style) + } + + /// Produces the style of a disabled text input. + fn disabled(&self, style: &Self::Style) -> Appearance; +} diff --git a/style/src/theme.rs b/style/src/theme.rs index 893d7202..a1501c01 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -17,6 +17,7 @@ use crate::rule; use crate::scrollable; use crate::slider; use crate::svg; +use crate::text_editor; use crate::text_input; use crate::toggler; @@ -1174,3 +1175,115 @@ impl text_input::StyleSheet for Theme { self.placeholder_color(style) } } + +/// The style of a text input. +#[derive(Default)] +pub enum TextEditor { + /// The default style. + #[default] + Default, + /// A custom style. + Custom(Box>), +} + +impl text_editor::StyleSheet for Theme { + type Style = TextEditor; + + fn active(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.active(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.background.strong.color, + } + } + + fn hovered(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.hovered(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.background.base.text, + } + } + + fn focused(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.focused(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.primary.strong.color, + } + } + + fn placeholder_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.placeholder_color(self); + } + + let palette = self.extended_palette(); + + palette.background.strong.color + } + + fn value_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.value_color(self); + } + + let palette = self.extended_palette(); + + palette.background.base.text + } + + fn selection_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.selection_color(self); + } + + let palette = self.extended_palette(); + + palette.primary.weak.color + } + + fn disabled(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.disabled(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.weak.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.background.strong.color, + } + } + + fn disabled_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.disabled_color(self); + } + + self.placeholder_color(style) + } +} diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 72184c8a..5f66dff2 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -383,6 +383,31 @@ impl Backend { clip_mask, ); } + Primitive::Editor { + editor, + position, + color, + } => { + let physical_bounds = + (Rectangle::new(*position, editor.bounds) + translation) + * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_editor( + editor, + *position + translation, + *color, + scale_factor, + pixels, + clip_mask, + ); + } Primitive::Text { content, bounds, diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 4f6e3941..d055c749 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -2,6 +2,7 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; use crate::core::{Color, Font, Pixels, Point, Rectangle}; use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::editor; use crate::graphics::text::font_system; use crate::graphics::text::paragraph; @@ -64,6 +65,37 @@ impl Pipeline { ); } + pub fn draw_editor( + &mut self, + editor: &editor::Weak, + position: Point, + color: Color, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + ) { + use crate::core::text::Editor as _; + + let Some(editor) = editor.upgrade() else { + return; + }; + + let mut font_system = font_system().write().expect("Write font system"); + + draw( + font_system.raw(), + &mut self.glyph_cache, + editor.buffer(), + Rectangle::new(position, editor.min_bounds()), + color, + alignment::Horizontal::Left, + alignment::Vertical::Top, + scale_factor, + pixels, + clip_mask, + ); + } + pub fn draw_cached( &mut self, content: &str, diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 7a5a0f7c..10b3332d 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -120,12 +120,25 @@ impl<'a> Layer<'a> { } => { let layer = &mut layers[current_layer]; - layer.text.push(Text::Managed { + layer.text.push(Text::Paragraph { paragraph: paragraph.clone(), position: *position + translation, color: *color, }); } + Primitive::Editor { + editor, + position, + color, + } => { + let layer = &mut layers[current_layer]; + + layer.text.push(Text::Editor { + editor: editor.clone(), + position: *position + translation, + color: *color, + }); + } Primitive::Text { content, bounds, diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index b61615d6..d46b39da 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -1,16 +1,22 @@ use crate::core::alignment; use crate::core::text; use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::graphics::text::editor; use crate::graphics::text::paragraph; /// A paragraph of text. #[derive(Debug, Clone)] pub enum Text<'a> { - Managed { + Paragraph { paragraph: paragraph::Weak, position: Point, color: Color, }, + Editor { + editor: editor::Weak, + position: Point, + color: Color, + }, Cached(Cached<'a>), } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 5c9f4d7e..397c38dd 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,7 +2,7 @@ use crate::core::alignment; use crate::core::{Rectangle, Size}; use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; -use crate::graphics::text::{font_system, Paragraph}; +use crate::graphics::text::{font_system, Editor, Paragraph}; use crate::layer::Text; use std::borrow::Cow; @@ -74,15 +74,19 @@ impl Pipeline { enum Allocation { Paragraph(Paragraph), + Editor(Editor), Cache(cache::KeyHash), } let allocations: Vec<_> = sections .iter() .map(|section| match section { - Text::Managed { paragraph, .. } => { + Text::Paragraph { paragraph, .. } => { paragraph.upgrade().map(Allocation::Paragraph) } + Text::Editor { editor, .. } => { + editor.upgrade().map(Allocation::Editor) + } Text::Cached(text) => { let (key, _) = cache.allocate( font_system, @@ -117,7 +121,7 @@ impl Pipeline { vertical_alignment, color, ) = match section { - Text::Managed { + Text::Paragraph { position, color, .. } => { use crate::core::text::Paragraph as _; @@ -135,6 +139,24 @@ impl Pipeline { *color, ) } + Text::Editor { + position, color, .. + } => { + use crate::core::text::Editor as _; + + let Some(Allocation::Editor(editor)) = allocation + else { + return None; + }; + + ( + editor.buffer(), + Rectangle::new(*position, editor.min_bounds()), + alignment::Horizontal::Left, + alignment::Vertical::Top, + *color, + ) + } Text::Cached(text) => { let Some(Allocation::Cache(key)) = allocation else { return None; diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 3c9c2b29..61541eac 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -16,6 +16,7 @@ use crate::runtime::Command; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; +use crate::text_editor::{self, TextEditor}; use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; @@ -206,6 +207,20 @@ where TextInput::new(placeholder, value) } +/// Creates a new [`TextEditor`]. +/// +/// [`TextEditor`]: crate::TextEditor +pub fn text_editor<'a, Message, Renderer>( + content: &'a text_editor::Content, +) -> TextEditor<'a, Message, Renderer> +where + Message: Clone, + Renderer: core::text::Renderer, + Renderer::Theme: text_editor::StyleSheet, +{ + TextEditor::new(content) +} + /// Creates a new [`Slider`]. /// /// [`Slider`]: crate::Slider diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 7e204171..f8e5e865 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -4,8 +4,8 @@ )] #![forbid(unsafe_code, rust_2018_idioms)] #![deny( - missing_debug_implementations, - missing_docs, + // missing_debug_implementations, + // missing_docs, unused_results, clippy::extra_unused_lifetimes, clippy::from_over_into, @@ -41,6 +41,7 @@ pub mod scrollable; pub mod slider; pub mod space; pub mod text; +pub mod text_editor; pub mod text_input; pub mod toggler; pub mod tooltip; diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs new file mode 100644 index 00000000..d09f2c3e --- /dev/null +++ b/widget/src/text_editor.rs @@ -0,0 +1,457 @@ +use crate::core::event::{self, Event}; +use crate::core::keyboard; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text::editor::{Cursor, Editor as _}; +use crate::core::text::{self, LineHeight}; +use crate::core::widget::{self, Widget}; +use crate::core::{ + Clipboard, Color, Element, Length, Padding, Pixels, Point, Rectangle, + Shell, Vector, +}; + +use std::cell::RefCell; + +pub use crate::style::text_editor::{Appearance, StyleSheet}; +pub use text::editor::Action; + +pub struct TextEditor<'a, Message, Renderer = crate::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + content: &'a Content, + font: Option, + text_size: Option, + line_height: LineHeight, + width: Length, + height: Length, + padding: Padding, + style: ::Style, + on_edit: Option Message + 'a>>, +} + +impl<'a, Message, Renderer> TextEditor<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + pub fn new(content: &'a Content) -> Self { + Self { + content, + font: None, + text_size: None, + line_height: LineHeight::default(), + width: Length::Fill, + height: Length::Fill, + padding: Padding::new(5.0), + style: Default::default(), + on_edit: None, + } + } + + pub fn on_edit(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self { + self.on_edit = Some(Box::new(on_edit)); + self + } + + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); + self + } + + pub fn padding(mut self, padding: impl Into) -> Self { + self.padding = padding.into(); + self + } +} + +pub struct Content(RefCell>) +where + R: text::Renderer; + +struct Internal +where + R: text::Renderer, +{ + editor: R::Editor, + is_dirty: bool, +} + +impl Content +where + R: text::Renderer, +{ + pub fn new() -> Self { + Self::with("") + } + + pub fn with(text: &str) -> Self { + Self(RefCell::new(Internal { + editor: R::Editor::with_text(text), + is_dirty: true, + })) + } + + pub fn edit(&mut self, action: Action) { + let internal = self.0.get_mut(); + + internal.editor.perform(action); + internal.is_dirty = true; + } +} + +impl Default for Content +where + Renderer: text::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +struct State { + is_focused: bool, + is_dragging: bool, + last_click: Option, +} + +impl<'a, Message, Renderer> Widget + for TextEditor<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::() + } + + fn state(&self) -> widget::tree::State { + widget::tree::State::new(State { + is_focused: false, + is_dragging: false, + last_click: None, + }) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _tree: &mut widget::Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> iced_renderer::core::layout::Node { + let mut internal = self.content.0.borrow_mut(); + + internal.editor.update( + limits.pad(self.padding).max(), + self.font.unwrap_or_else(|| renderer.default_font()), + self.text_size.unwrap_or_else(|| renderer.default_size()), + self.line_height, + ); + + layout::Node::new(limits.max()) + } + + fn on_event( + &mut self, + tree: &mut widget::Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + let Some(on_edit) = self.on_edit.as_ref() else { + return event::Status::Ignored; + }; + + let state = tree.state.downcast_mut::(); + + let Some(update) = Update::from_event( + event, + state, + layout.bounds(), + self.padding, + cursor, + ) else { + return event::Status::Ignored; + }; + + match update { + Update::Focus { click, action } => { + state.is_focused = true; + state.last_click = Some(click); + shell.publish(on_edit(action)); + } + Update::Unfocus => { + state.is_focused = false; + state.is_dragging = false; + } + Update::Click { click, action } => { + state.last_click = Some(click); + state.is_dragging = true; + shell.publish(on_edit(action)); + } + Update::StopDragging => { + state.is_dragging = false; + } + Update::Edit(action) => { + shell.publish(on_edit(action)); + } + Update::Copy => {} + Update::Paste => if let Some(_contents) = clipboard.read() {}, + } + + event::Status::Captured + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + theme: &::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + let internal = self.content.0.borrow(); + let state = tree.state.downcast_ref::(); + + let is_disabled = self.on_edit.is_none(); + let is_mouse_over = cursor.is_over(bounds); + + let appearance = if is_disabled { + theme.disabled(&self.style) + } else if state.is_focused { + theme.focused(&self.style) + } else if is_mouse_over { + theme.hovered(&self.style) + } else { + theme.active(&self.style) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: appearance.border_radius, + border_width: appearance.border_width, + border_color: appearance.border_color, + }, + appearance.background, + ); + + renderer.fill_editor( + &internal.editor, + bounds.position() + + Vector::new(self.padding.left, self.padding.top), + style.text_color, + ); + + if state.is_focused { + match internal.editor.cursor() { + Cursor::Caret(position) => { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: position.x + bounds.x + self.padding.left, + y: position.y + bounds.y + self.padding.top, + width: 1.0, + height: self + .line_height + .to_absolute(self.text_size.unwrap_or_else( + || renderer.default_size(), + )) + .into(), + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.value_color(&self.style), + ); + } + Cursor::Selection(ranges) => { + for range in ranges { + renderer.fill_quad( + renderer::Quad { + bounds: range + Vector::new(bounds.x, bounds.y), + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.selection_color(&self.style), + ); + } + } + } + } + } + + fn mouse_interaction( + &self, + _state: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &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() + } + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from(text_editor: TextEditor<'a, Message, Renderer>) -> Self { + Self::new(text_editor) + } +} + +enum Update { + Focus { click: mouse::Click, action: Action }, + Unfocus, + Click { click: mouse::Click, action: Action }, + StopDragging, + Edit(Action), + Copy, + Paste, +} + +impl Update { + fn from_event( + event: Event, + state: &State, + bounds: Rectangle, + padding: Padding, + cursor: mouse::Cursor, + ) -> Option { + 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.top, padding.left); + + if state.is_focused { + let click = mouse::Click::new( + cursor_position, + state.last_click, + ); + + let action = match click.kind() { + mouse::click::Kind::Single => { + Action::Click(cursor_position) + } + mouse::click::Kind::Double => { + Action::SelectWord + } + mouse::click::Kind::Triple => { + Action::SelectLine + } + }; + + Some(Update::Click { click, action }) + } else { + Some(Update::Focus { + click: mouse::Click::new(cursor_position, None), + action: Action::Click(cursor_position), + }) + } + } else if state.is_focused { + Some(Update::Unfocus) + } else { + None + } + } + mouse::Event::ButtonReleased(mouse::Button::Left) => { + Some(Update::StopDragging) + } + mouse::Event::CursorMoved { .. } if state.is_dragging => { + let cursor_position = cursor.position_in(bounds)? + - Vector::new(padding.top, padding.left); + + Some(Self::Edit(Action::Drag(cursor_position))) + } + _ => None, + }, + Event::Keyboard(event) => match event { + keyboard::Event::KeyPressed { + key_code, + modifiers, + } if state.is_focused => match key_code { + keyboard::KeyCode::Left => { + if platform::is_jump_modifier_pressed(modifiers) { + Some(Self::Edit(Action::MoveLeftWord)) + } else { + Some(Self::Edit(Action::MoveLeft)) + } + } + keyboard::KeyCode::Right => { + if platform::is_jump_modifier_pressed(modifiers) { + Some(Self::Edit(Action::MoveRightWord)) + } else { + Some(Self::Edit(Action::MoveRight)) + } + } + keyboard::KeyCode::Up => Some(Self::Edit(Action::MoveUp)), + keyboard::KeyCode::Down => { + Some(Self::Edit(Action::MoveDown)) + } + keyboard::KeyCode::Backspace => { + Some(Self::Edit(Action::Backspace)) + } + keyboard::KeyCode::Delete => { + Some(Self::Edit(Action::Delete)) + } + keyboard::KeyCode::Escape => Some(Self::Unfocus), + _ => None, + }, + keyboard::Event::CharacterReceived(c) if state.is_focused => { + Some(Self::Edit(Action::Insert(c))) + } + _ => None, + }, + _ => None, + } + } +} + +mod platform { + use crate::core::keyboard; + + pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { + if cfg!(target_os = "macos") { + modifiers.alt() + } else { + modifiers.control() + } + } +} -- cgit From 1455911b636f19810e12eeb12a6eed11c5244cfe Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 12 Sep 2023 15:03:23 +0200 Subject: Add `Enter` variant to `Action` in `text::Editor` --- core/src/text/editor.rs | 1 + graphics/src/text/editor.rs | 1 + widget/src/text_editor.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index a4fd0ec1..09d4efde 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -51,6 +51,7 @@ pub enum Action { SelectWord, SelectLine, Insert(char), + Enter, Backspace, Delete, Click(Point), diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 53f63fea..b4d6819f 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -176,6 +176,7 @@ impl editor::Editor for Editor { Action::MoveUp => act(cosmic_text::Action::Up), Action::MoveDown => act(cosmic_text::Action::Down), Action::Insert(c) => act(cosmic_text::Action::Insert(c)), + Action::Enter => act(cosmic_text::Action::Enter), Action::Backspace => act(cosmic_text::Action::Backspace), Action::Delete => act(cosmic_text::Action::Delete), Action::Click(position) => act(cosmic_text::Action::Click { diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index d09f2c3e..fcbd3dad 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -425,6 +425,7 @@ impl Update { keyboard::KeyCode::Down => { Some(Self::Edit(Action::MoveDown)) } + keyboard::KeyCode::Enter => Some(Self::Edit(Action::Enter)), keyboard::KeyCode::Backspace => { Some(Self::Edit(Action::Backspace)) } -- cgit From abab1448576fbfa4717b65cdf1455debf44f2df5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 12 Sep 2023 18:20:02 +0200 Subject: Return `Cursor::Caret` if selection matches cursor position in `Editor::cursor` --- graphics/src/text/editor.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index b4d6819f..7b0ddec1 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -72,15 +72,18 @@ impl editor::Editor for Editor { fn cursor(&self) -> editor::Cursor { let internal = self.internal(); + let cursor = internal.editor.cursor(); + let buffer = internal.editor.buffer(); + match internal.editor.select_opt() { - Some(selection) => { + Some(selection) + if cursor.line != selection.line + || cursor.index != selection.index => + { // TODO Cursor::Selection(vec![]) } - None => { - let cursor = internal.editor.cursor(); - let buffer = internal.editor.buffer(); - + _ => { let lines_before_cursor: usize = buffer .lines .iter() -- cgit From 4389ab9865d13e17ce3c66223d7c149437be692b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 12 Sep 2023 18:27:30 +0200 Subject: Fix cursor offset with `Affinity::After` at the end of lines in `Editor::cursor` --- graphics/src/text/editor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 7b0ddec1..b39e9831 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -143,7 +143,10 @@ impl editor::Editor for Editor { None } }) - .unwrap_or((0, 0.0)); + .unwrap_or(( + 0, + layout.last().map(|line| line.w).unwrap_or(0.0), + )); let line_height = buffer.metrics().line_height; -- cgit From a28ed825c1f48c61a655c5583eb207999e98f400 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 12 Sep 2023 20:57:46 +0200 Subject: Fix subline positioning in `Editor::cursor` --- graphics/src/text/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index b39e9831..52a5d942 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -144,7 +144,7 @@ impl editor::Editor for Editor { } }) .unwrap_or(( - 0, + layout.len().saturating_sub(1), layout.last().map(|line| line.w).unwrap_or(0.0), )); -- cgit From 40eb648f1e1e2ceb2782eddacbbc966f44de6961 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 13 Sep 2023 15:00:33 +0200 Subject: Implement `Cursor::Selection` calculation in `Editor::cursor` --- graphics/src/text/editor.rs | 118 ++++++++++++++++++++++++++++++++++++++++++-- widget/src/text_editor.rs | 6 ++- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 52a5d942..3544bde6 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,6 +1,6 @@ use crate::core::text::editor::{self, Action, Cursor}; use crate::core::text::LineHeight; -use crate::core::{Font, Pixels, Point, Size}; +use crate::core::{Font, Pixels, Point, Rectangle, Size, Vector}; use crate::text; use cosmic_text::Edit; @@ -80,8 +80,70 @@ impl editor::Editor for Editor { if cursor.line != selection.line || cursor.index != selection.index => { - // TODO - Cursor::Selection(vec![]) + let line_height = buffer.metrics().line_height; + let scroll_offset = buffer.scroll() as f32 * line_height; + + let (start, end) = if cursor < selection { + (cursor, selection) + } else { + (selection, cursor) + }; + + let visual_lines_before_start: usize = buffer + .lines + .iter() + .take(start.line) + .map(|line| { + line.layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() + }) + .sum(); + + let selected_lines = end.line - start.line + 1; + + let regions = buffer + .lines + .iter() + .skip(start.line) + .take(selected_lines) + .enumerate() + .flat_map(|(i, line)| { + highlight_line( + line, + if i == 0 { start.index } else { 0 }, + if i == selected_lines - 1 { + end.index + } else { + line.text().len() + }, + ) + }) + .enumerate() + .filter_map(|(visual_line, (x, width))| { + if width > 0.0 { + Some(Rectangle { + x, + width, + y: visual_line as f32 * line_height, + height: line_height, + }) + } else { + None + } + }) + .map(|region| { + region + + Vector::new( + 0.0, + visual_lines_before_start as f32 * line_height + + scroll_offset, + ) + }) + .collect(); + + Cursor::Selection(regions) } _ => { let lines_before_cursor: usize = buffer @@ -332,3 +394,53 @@ impl PartialEq for Weak { } } } + +fn highlight_line<'a>( + line: &'a cosmic_text::BufferLine, + from: usize, + to: usize, +) -> impl Iterator + 'a { + let layout = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached"); + + layout.iter().map(move |visual_line| { + let start = visual_line + .glyphs + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = visual_line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let range = start.max(from)..end.min(to); + + if range.is_empty() { + (0.0, 0.0) + } else if range.start == start && range.end == end { + (0.0, visual_line.w) + } else { + let first_glyph = visual_line + .glyphs + .iter() + .position(|glyph| range.start <= glyph.start) + .unwrap_or(0); + + let mut glyphs = visual_line.glyphs.iter(); + + let x = + glyphs.by_ref().take(first_glyph).map(|glyph| glyph.w).sum(); + + let width: f32 = glyphs + .take_while(|glyph| range.end > glyph.start) + .map(|glyph| glyph.w) + .sum(); + + (x, width) + } + }) +} diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index fcbd3dad..12e66f68 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -288,7 +288,11 @@ where for range in ranges { renderer.fill_quad( renderer::Quad { - bounds: range + Vector::new(bounds.x, bounds.y), + bounds: range + + Vector::new( + bounds.x + self.padding.left, + bounds.y + self.padding.top, + ), border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, -- cgit From d502c9f16fc78bf6b5253152751480c5b5e5999c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 13 Sep 2023 15:16:47 +0200 Subject: Unify `Focus` and `Click` updates in `widget::text_editor` --- widget/src/text_editor.rs | 48 ++++++++++++++++------------------------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 12e66f68..a8069069 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -189,16 +189,12 @@ where }; match update { - Update::Focus { click, action } => { - state.is_focused = true; - state.last_click = Some(click); - shell.publish(on_edit(action)); - } Update::Unfocus => { state.is_focused = false; state.is_dragging = false; } Update::Click { click, action } => { + state.is_focused = true; state.last_click = Some(click); state.is_dragging = true; shell.publish(on_edit(action)); @@ -340,9 +336,8 @@ where } enum Update { - Focus { click: mouse::Click, action: Action }, - Unfocus, Click { click: mouse::Click, action: Action }, + Unfocus, StopDragging, Edit(Action), Copy, @@ -364,31 +359,20 @@ impl Update { let cursor_position = cursor_position - Vector::new(padding.top, padding.left); - if state.is_focused { - let click = mouse::Click::new( - cursor_position, - state.last_click, - ); - - let action = match click.kind() { - mouse::click::Kind::Single => { - Action::Click(cursor_position) - } - mouse::click::Kind::Double => { - Action::SelectWord - } - mouse::click::Kind::Triple => { - Action::SelectLine - } - }; - - Some(Update::Click { click, action }) - } else { - Some(Update::Focus { - click: mouse::Click::new(cursor_position, None), - action: Action::Click(cursor_position), - }) - } + let click = mouse::Click::new( + cursor_position, + state.last_click, + ); + + let action = match click.kind() { + mouse::click::Kind::Single => { + Action::Click(cursor_position) + } + mouse::click::Kind::Double => Action::SelectWord, + mouse::click::Kind::Triple => Action::SelectLine, + }; + + Some(Update::Click { click, action }) } else if state.is_focused { Some(Update::Unfocus) } else { -- cgit From 52b36a9574f45138363a4bfc6394c6da03baa433 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 13 Sep 2023 15:17:04 +0200 Subject: Use `Theme::Dark` in `editor` example --- examples/editor/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 50989ac5..2a70b34c 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{container, text_editor}; -use iced::{Element, Font, Sandbox, Settings}; +use iced::{Element, Font, Sandbox, Settings, Theme}; pub fn main() -> iced::Result { Editor::run(Settings::default()) @@ -46,4 +46,8 @@ impl Sandbox for Editor { .padding(20) .into() } + + fn theme(&self) -> Theme { + Theme::Dark + } } -- cgit From f4c51a96d50953d5fb6e9eb62194f226e2cbfd3c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 13 Sep 2023 16:11:43 +0200 Subject: Introduce `Motion` concept in `core::text::editor` --- core/src/text/editor.rs | 38 ++++++++++++---- graphics/src/text/editor.rs | 106 ++++++++++++++++++++++++++++++++++---------- widget/src/text_editor.rs | 77 ++++++++++++++++++-------------- 3 files changed, 156 insertions(+), 65 deletions(-) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 09d4efde..f87e18f3 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -40,14 +40,8 @@ pub trait Editor: Sized + Default { #[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { - MoveLeft, - MoveRight, - MoveUp, - MoveDown, - MoveLeftWord, - MoveRightWord, - MoveHome, - MoveEnd, + Move(Motion), + Select(Motion), SelectWord, SelectLine, Insert(char), @@ -58,6 +52,34 @@ pub enum Action { Drag(Point), } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Motion { + Left, + Right, + Up, + Down, + WordLeft, + WordRight, + Home, + End, + PageUp, + PageDown, + DocumentStart, + DocumentEnd, +} + +impl Motion { + pub fn widen(self) -> Self { + match self { + Self::Left => Self::WordLeft, + Self::Right => Self::WordRight, + Self::Home => Self::DocumentStart, + Self::End => Self::DocumentEnd, + _ => self, + } + } +} + /// The cursor of an [`Editor`]. #[derive(Debug, Clone)] pub enum Cursor { diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 3544bde6..747f3a80 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,4 +1,4 @@ -use crate::core::text::editor::{self, Action, Cursor}; +use crate::core::text::editor::{self, Action, Cursor, Motion}; use crate::core::text::LineHeight; use crate::core::{Font, Pixels, Point, Rectangle, Size, Vector}; use crate::text; @@ -76,10 +76,7 @@ impl editor::Editor for Editor { let buffer = internal.editor.buffer(); match internal.editor.select_opt() { - Some(selection) - if cursor.line != selection.line - || cursor.index != selection.index => - { + Some(selection) => { let line_height = buffer.metrics().line_height; let scroll_offset = buffer.scroll() as f32 * line_height; @@ -236,26 +233,87 @@ impl editor::Editor for Editor { let editor = &mut internal.editor; - let mut act = |action| editor.action(font_system.raw(), action); - match action { - Action::MoveLeft => act(cosmic_text::Action::Left), - Action::MoveRight => act(cosmic_text::Action::Right), - Action::MoveUp => act(cosmic_text::Action::Up), - Action::MoveDown => act(cosmic_text::Action::Down), - Action::Insert(c) => act(cosmic_text::Action::Insert(c)), - Action::Enter => act(cosmic_text::Action::Enter), - Action::Backspace => act(cosmic_text::Action::Backspace), - Action::Delete => act(cosmic_text::Action::Delete), - Action::Click(position) => act(cosmic_text::Action::Click { - x: position.x as i32, - y: position.y as i32, - }), - Action::Drag(position) => act(cosmic_text::Action::Drag { - x: position.x as i32, - y: position.y as i32, - }), - _ => todo!(), + // Motion events + Action::Move(motion) => { + if let Some(_selection) = editor.select_opt() { + editor.set_select_opt(None); + } else { + editor.action( + font_system.raw(), + match motion { + Motion::Left => cosmic_text::Action::Left, + Motion::Right => cosmic_text::Action::Right, + Motion::Up => cosmic_text::Action::Up, + Motion::Down => cosmic_text::Action::Down, + Motion::WordLeft => cosmic_text::Action::LeftWord, + Motion::WordRight => cosmic_text::Action::RightWord, + Motion::Home => cosmic_text::Action::Home, + Motion::End => cosmic_text::Action::End, + Motion::PageUp => cosmic_text::Action::PageUp, + Motion::PageDown => cosmic_text::Action::PageDown, + Motion::DocumentStart => { + cosmic_text::Action::BufferStart + } + Motion::DocumentEnd => { + cosmic_text::Action::BufferEnd + } + }, + ); + } + } + + // Selection events + Action::Select(_motion) => todo!(), + Action::SelectWord => todo!(), + Action::SelectLine => todo!(), + + // Editing events + Action::Insert(c) => { + editor + .action(font_system.raw(), cosmic_text::Action::Insert(c)); + } + Action::Enter => { + editor.action(font_system.raw(), cosmic_text::Action::Enter); + } + Action::Backspace => { + editor + .action(font_system.raw(), cosmic_text::Action::Backspace); + } + Action::Delete => { + editor.action(font_system.raw(), cosmic_text::Action::Delete); + } + + // Mouse events + Action::Click(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Click { + x: position.x as i32, + y: position.y as i32, + }, + ); + } + Action::Drag(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Drag { + x: position.x as i32, + y: position.y as i32, + }, + ); + + // Deselect if selection matches cursor position + if let Some(selection) = editor.select_opt() { + let cursor = editor.cursor(); + + if cursor.line == selection.line + && cursor.index == selection.index + { + editor.set_select_opt(None); + } + } + } } editor.shape_as_needed(font_system.raw()); diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index a8069069..38c243bd 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -14,7 +14,7 @@ use crate::core::{ use std::cell::RefCell; pub use crate::style::text_editor::{Appearance, StyleSheet}; -pub use text::editor::Action; +pub use text::editor::{Action, Motion}; pub struct TextEditor<'a, Message, Renderer = crate::Renderer> where @@ -189,16 +189,16 @@ where }; match update { - Update::Unfocus => { - state.is_focused = false; - state.is_dragging = false; - } Update::Click { click, action } => { state.is_focused = true; - state.last_click = Some(click); state.is_dragging = true; + state.last_click = Some(click); shell.publish(on_edit(action)); } + Update::Unfocus => { + state.is_focused = false; + state.is_dragging = false; + } Update::StopDragging => { state.is_dragging = false; } @@ -352,6 +352,9 @@ impl Update { padding: Padding, cursor: mouse::Cursor, ) -> Option { + let edit = |action| Some(Update::Edit(action)); + let move_ = |motion| Some(Update::Edit(Action::Move(motion))); + match event { Event::Mouse(event) => match event { mouse::Event::ButtonPressed(mouse::Button::Left) => { @@ -386,7 +389,7 @@ impl Update { let cursor_position = cursor.position_in(bounds)? - Vector::new(padding.top, padding.left); - Some(Self::Edit(Action::Drag(cursor_position))) + edit(Action::Drag(cursor_position)) } _ => None, }, @@ -394,37 +397,31 @@ impl Update { keyboard::Event::KeyPressed { key_code, modifiers, - } if state.is_focused => match key_code { - keyboard::KeyCode::Left => { - if platform::is_jump_modifier_pressed(modifiers) { - Some(Self::Edit(Action::MoveLeftWord)) + } if state.is_focused => { + if let Some(motion) = motion(key_code) { + let motion = if modifiers.control() { + motion.widen() } else { - Some(Self::Edit(Action::MoveLeft)) - } - } - keyboard::KeyCode::Right => { - if platform::is_jump_modifier_pressed(modifiers) { - Some(Self::Edit(Action::MoveRightWord)) + motion + }; + + return edit(if modifiers.shift() { + Action::Select(motion) } else { - Some(Self::Edit(Action::MoveRight)) - } - } - keyboard::KeyCode::Up => Some(Self::Edit(Action::MoveUp)), - keyboard::KeyCode::Down => { - Some(Self::Edit(Action::MoveDown)) - } - keyboard::KeyCode::Enter => Some(Self::Edit(Action::Enter)), - keyboard::KeyCode::Backspace => { - Some(Self::Edit(Action::Backspace)) + Action::Move(motion) + }); } - keyboard::KeyCode::Delete => { - Some(Self::Edit(Action::Delete)) + + match key_code { + keyboard::KeyCode::Enter => edit(Action::Enter), + keyboard::KeyCode::Backspace => edit(Action::Backspace), + keyboard::KeyCode::Delete => edit(Action::Delete), + keyboard::KeyCode::Escape => Some(Self::Unfocus), + _ => None, } - keyboard::KeyCode::Escape => Some(Self::Unfocus), - _ => None, - }, + } keyboard::Event::CharacterReceived(c) if state.is_focused => { - Some(Self::Edit(Action::Insert(c))) + edit(Action::Insert(c)) } _ => None, }, @@ -433,6 +430,20 @@ impl Update { } } +fn motion(key_code: keyboard::KeyCode) -> Option { + match key_code { + keyboard::KeyCode::Left => Some(Motion::Left), + keyboard::KeyCode::Right => Some(Motion::Right), + keyboard::KeyCode::Up => Some(Motion::Up), + keyboard::KeyCode::Down => Some(Motion::Down), + keyboard::KeyCode::Home => Some(Motion::Home), + keyboard::KeyCode::End => Some(Motion::End), + keyboard::KeyCode::PageUp => Some(Motion::PageUp), + keyboard::KeyCode::PageDown => Some(Motion::PageDown), + _ => None, + } +} + mod platform { use crate::core::keyboard; -- cgit From f14ef7a6069cf45ae11261d7d20df6a5d7870dde Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 13 Sep 2023 16:31:56 +0200 Subject: Fix `clippy` lints --- graphics/src/text/editor.rs | 27 ++++++++++++++++++--------- widget/src/helpers.rs | 6 +++--- widget/src/text_editor.rs | 30 ++++++++++++++++++++---------- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 747f3a80..d31ea390 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -25,7 +25,7 @@ impl Editor { } pub fn buffer(&self) -> &cosmic_text::Buffer { - &self.internal().editor.buffer() + self.internal().editor.buffer() } pub fn downgrade(&self) -> Weak { @@ -53,11 +53,11 @@ impl editor::Editor for Editor { line_height: 1.0, }); + let mut font_system = + text::font_system().write().expect("Write font system"); + buffer.set_text( - text::font_system() - .write() - .expect("Write font system") - .raw(), + font_system.raw(), text, cosmic_text::Attrs::new(), cosmic_text::Shaping::Advanced, @@ -65,6 +65,7 @@ impl editor::Editor for Editor { Editor(Some(Arc::new(Internal { editor: cosmic_text::Editor::new(buffer), + version: font_system.version(), ..Default::default() }))) } @@ -347,6 +348,14 @@ impl editor::Editor for Editor { let mut changed = false; + if font_system.version() != internal.version { + for line in internal.editor.buffer_mut().lines.iter_mut() { + line.reset(); + } + + changed = true; + } + if new_font != internal.font { for line in internal.editor.buffer_mut().lines.iter_mut() { let _ = line.set_attrs_list(cosmic_text::AttrsList::new( @@ -383,7 +392,7 @@ impl editor::Editor for Editor { } if changed { - internal.min_bounds = text::measure(&internal.editor.buffer()); + internal.min_bounds = text::measure(internal.editor.buffer()); } self.0 = Some(Arc::new(internal)); @@ -453,11 +462,11 @@ impl PartialEq for Weak { } } -fn highlight_line<'a>( - line: &'a cosmic_text::BufferLine, +fn highlight_line( + line: &cosmic_text::BufferLine, from: usize, to: usize, -) -> impl Iterator + 'a { +) -> impl Iterator + '_ { let layout = line .layout_opt() .as_ref() diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 61541eac..e3f31513 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -210,9 +210,9 @@ where /// Creates a new [`TextEditor`]. /// /// [`TextEditor`]: crate::TextEditor -pub fn text_editor<'a, Message, Renderer>( - content: &'a text_editor::Content, -) -> TextEditor<'a, Message, Renderer> +pub fn text_editor( + content: &text_editor::Content, +) -> TextEditor<'_, Message, Renderer> where Message: Clone, Renderer: core::text::Renderer, diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 38c243bd..48de6409 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -7,8 +7,8 @@ use crate::core::text::editor::{Cursor, Editor as _}; use crate::core::text::{self, LineHeight}; use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Color, Element, Length, Padding, Pixels, Point, Rectangle, - Shell, Vector, + Clipboard, Color, Element, Length, Padding, Pixels, Rectangle, Shell, + Vector, }; use std::cell::RefCell; @@ -205,8 +205,12 @@ where Update::Edit(action) => { shell.publish(on_edit(action)); } - Update::Copy => {} - Update::Paste => if let Some(_contents) = clipboard.read() {}, + Update::Copy => todo!(), + Update::Paste => { + if let Some(_contents) = clipboard.read() { + todo!() + } + } } event::Status::Captured @@ -353,7 +357,6 @@ impl Update { cursor: mouse::Cursor, ) -> Option { let edit = |action| Some(Update::Edit(action)); - let move_ = |motion| Some(Update::Edit(Action::Move(motion))); match event { Event::Mouse(event) => match event { @@ -399,11 +402,12 @@ impl Update { modifiers, } if state.is_focused => { if let Some(motion) = motion(key_code) { - let motion = if modifiers.control() { - motion.widen() - } else { - motion - }; + let motion = + if platform::is_jump_modifier_pressed(modifiers) { + motion.widen() + } else { + motion + }; return edit(if modifiers.shift() { Action::Select(motion) @@ -417,6 +421,12 @@ impl Update { keyboard::KeyCode::Backspace => edit(Action::Backspace), keyboard::KeyCode::Delete => edit(Action::Delete), keyboard::KeyCode::Escape => Some(Self::Unfocus), + keyboard::KeyCode::C => Some(Self::Copy), + keyboard::KeyCode::V + if modifiers.command() && !modifiers.alt() => + { + Some(Self::Paste) + } _ => None, } } -- cgit From c829b4b04e1274f157ea7bb3adf832c4c53ce3e8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 13 Sep 2023 17:55:33 +0200 Subject: Fix unused import in `iced_renderer` --- renderer/src/lib.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 6f044af6..81f60886 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -29,9 +29,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::text::{self, Text}; -use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Vector, -}; +use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector}; use crate::graphics::text::Editor; use crate::graphics::text::Paragraph; use crate::graphics::Mesh; @@ -219,7 +217,10 @@ impl text::Renderer for Renderer { impl crate::core::image::Renderer for Renderer { type Handle = crate::core::image::Handle; - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size { + fn dimensions( + &self, + handle: &crate::core::image::Handle, + ) -> core::Size { delegate!(self, renderer, renderer.dimensions(handle)) } @@ -230,7 +231,7 @@ impl crate::core::image::Renderer for Renderer { #[cfg(feature = "svg")] impl crate::core::svg::Renderer for Renderer { - fn dimensions(&self, handle: &crate::core::svg::Handle) -> Size { + fn dimensions(&self, handle: &crate::core::svg::Handle) -> core::Size { delegate!(self, renderer, renderer.dimensions(handle)) } -- cgit From ab020383b9fd7f2cc15d145dd1a3c0870dc71d8b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 14 Sep 2023 00:41:15 +0200 Subject: Fix scrolling offset for `Cursor::Selection` --- graphics/src/text/editor.rs | 74 +++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index d31ea390..c0f8d9d5 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,6 +1,6 @@ use crate::core::text::editor::{self, Action, Cursor, Motion}; use crate::core::text::LineHeight; -use crate::core::{Font, Pixels, Point, Rectangle, Size, Vector}; +use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; use cosmic_text::Edit; @@ -78,29 +78,18 @@ impl editor::Editor for Editor { match internal.editor.select_opt() { Some(selection) => { - let line_height = buffer.metrics().line_height; - let scroll_offset = buffer.scroll() as f32 * line_height; - let (start, end) = if cursor < selection { (cursor, selection) } else { (selection, cursor) }; - let visual_lines_before_start: usize = buffer - .lines - .iter() - .take(start.line) - .map(|line| { - line.layout_opt() - .as_ref() - .expect("Line layout should be cached") - .len() - }) - .sum(); - + let line_height = buffer.metrics().line_height; let selected_lines = end.line - start.line + 1; + let visual_lines_offset = + visual_lines_offset(start.line, buffer); + let regions = buffer .lines .iter() @@ -124,37 +113,24 @@ impl editor::Editor for Editor { Some(Rectangle { x, width, - y: visual_line as f32 * line_height, + y: (visual_line as i32 + visual_lines_offset) + as f32 + * line_height, height: line_height, }) } else { None } }) - .map(|region| { - region - + Vector::new( - 0.0, - visual_lines_before_start as f32 * line_height - + scroll_offset, - ) - }) .collect(); Cursor::Selection(regions) } _ => { - let lines_before_cursor: usize = buffer - .lines - .iter() - .take(cursor.line) - .map(|line| { - line.layout_opt() - .as_ref() - .expect("Line layout should be cached") - .len() - }) - .sum(); + let line_height = buffer.metrics().line_height; + + let visual_lines_offset = + visual_lines_offset(cursor.line, buffer); let line = buffer .lines @@ -168,7 +144,7 @@ impl editor::Editor for Editor { let mut lines = layout.iter().enumerate(); - let (subline, offset) = lines + let (visual_line, offset) = lines .find_map(|(i, line)| { let start = line .glyphs @@ -208,14 +184,10 @@ impl editor::Editor for Editor { layout.last().map(|line| line.w).unwrap_or(0.0), )); - let line_height = buffer.metrics().line_height; - - let scroll_offset = buffer.scroll() as f32 * line_height; - Cursor::Caret(Point::new( offset, - (lines_before_cursor + subline) as f32 * line_height - - scroll_offset, + (visual_lines_offset + visual_line as i32) as f32 + * line_height, )) } } @@ -511,3 +483,19 @@ fn highlight_line( } }) } + +fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { + let visual_lines_before_start: usize = buffer + .lines + .iter() + .take(line) + .map(|line| { + line.layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() + }) + .sum(); + + visual_lines_before_start as i32 - buffer.scroll() +} -- cgit From e6c2db8a9312e3fe37f30f049d1fa497892f1a86 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 14 Sep 2023 00:47:04 +0200 Subject: Fix `Cursor::Caret` position on lines that wrap on whitespace --- graphics/src/text/editor.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c0f8d9d5..83d41c85 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -157,7 +157,7 @@ impl editor::Editor for Editor { .map(|glyph| glyph.end) .unwrap_or(0); - let is_cursor_after_start = start <= cursor.index; + let is_cursor_before_start = start > cursor.index; let is_cursor_before_end = match cursor.affinity { cosmic_text::Affinity::Before => { @@ -166,7 +166,17 @@ impl editor::Editor for Editor { cosmic_text::Affinity::After => cursor.index < end, }; - if is_cursor_after_start && is_cursor_before_end { + if is_cursor_before_start { + // Sometimes, the glyph we are looking for is right + // between lines. This can happen when a line wraps + // on a space. + // In that case, we can assume the cursor is at the + // end of the previous line. + // i is guaranteed to be > 0 because `start` is always + // 0 for the first line, so there is no way for the + // cursor to be before it. + Some((i - 1, layout[i - 1].w)) + } else if is_cursor_before_end { let offset = line .glyphs .iter() -- cgit From b24b94d82778733ddae1b824d0d7690afcec3056 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 14 Sep 2023 14:18:49 +0200 Subject: Handle motions when a selection is present in `text::Editor` --- core/src/text/editor.rs | 23 +++++++++++++++ graphics/src/text/editor.rs | 70 ++++++++++++++++++++++++++++++--------------- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index f87e18f3..3adfc61a 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -78,6 +78,29 @@ impl Motion { _ => self, } } + + pub fn direction(&self) -> Direction { + match self { + Self::Left + | Self::Up + | Self::WordLeft + | Self::Home + | Self::PageUp + | Self::DocumentStart => Direction::Left, + Self::Right + | Self::Down + | Self::WordRight + | Self::End + | Self::PageDown + | Self::DocumentEnd => Direction::Right, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Left, + Right, } /// The cursor of an [`Editor`]. diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 83d41c85..d88bcd1d 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,4 +1,4 @@ -use crate::core::text::editor::{self, Action, Cursor, Motion}; +use crate::core::text::editor::{self, Action, Cursor, Direction, Motion}; use crate::core::text::LineHeight; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; @@ -219,30 +219,37 @@ impl editor::Editor for Editor { match action { // Motion events Action::Move(motion) => { - if let Some(_selection) = editor.select_opt() { + if let Some(selection) = editor.select_opt() { + let cursor = editor.cursor(); + + let (left, right) = if cursor < selection { + (cursor, selection) + } else { + (selection, cursor) + }; + editor.set_select_opt(None); + + match motion { + // These motions are performed as-is even when a selection + // is present + Motion::Home + | Motion::End + | Motion::DocumentStart + | Motion::DocumentEnd => { + editor.action( + font_system.raw(), + motion_to_action(motion), + ); + } + // Other motions simply move the cursor to one end of the selection + _ => editor.set_cursor(match motion.direction() { + Direction::Left => left, + Direction::Right => right, + }), + } } else { - editor.action( - font_system.raw(), - match motion { - Motion::Left => cosmic_text::Action::Left, - Motion::Right => cosmic_text::Action::Right, - Motion::Up => cosmic_text::Action::Up, - Motion::Down => cosmic_text::Action::Down, - Motion::WordLeft => cosmic_text::Action::LeftWord, - Motion::WordRight => cosmic_text::Action::RightWord, - Motion::Home => cosmic_text::Action::Home, - Motion::End => cosmic_text::Action::End, - Motion::PageUp => cosmic_text::Action::PageUp, - Motion::PageDown => cosmic_text::Action::PageDown, - Motion::DocumentStart => { - cosmic_text::Action::BufferStart - } - Motion::DocumentEnd => { - cosmic_text::Action::BufferEnd - } - }, - ); + editor.action(font_system.raw(), motion_to_action(motion)); } } @@ -509,3 +516,20 @@ fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { visual_lines_before_start as i32 - buffer.scroll() } + +fn motion_to_action(motion: Motion) -> cosmic_text::Action { + match motion { + Motion::Left => cosmic_text::Action::Left, + Motion::Right => cosmic_text::Action::Right, + Motion::Up => cosmic_text::Action::Up, + Motion::Down => cosmic_text::Action::Down, + Motion::WordLeft => cosmic_text::Action::LeftWord, + Motion::WordRight => cosmic_text::Action::RightWord, + Motion::Home => cosmic_text::Action::Home, + Motion::End => cosmic_text::Action::End, + Motion::PageUp => cosmic_text::Action::PageUp, + Motion::PageDown => cosmic_text::Action::PageDown, + Motion::DocumentStart => cosmic_text::Action::BufferStart, + Motion::DocumentEnd => cosmic_text::Action::BufferEnd, + } +} -- cgit From edd591847599a3e47601646ce075cb5b71ea751b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 14 Sep 2023 14:25:46 +0200 Subject: Implement motion selection in `text::Editor` --- graphics/src/text/editor.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index d88bcd1d..c6b2abd5 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -254,7 +254,26 @@ impl editor::Editor for Editor { } // Selection events - Action::Select(_motion) => todo!(), + Action::Select(motion) => { + let cursor = editor.cursor(); + + if editor.select_opt().is_none() { + editor.set_select_opt(Some(cursor)); + } + + editor.action(font_system.raw(), motion_to_action(motion)); + + // Deselect if selection matches cursor position + if let Some(selection) = editor.select_opt() { + let cursor = editor.cursor(); + + if cursor.line == selection.line + && cursor.index == selection.index + { + editor.set_select_opt(None); + } + } + } Action::SelectWord => todo!(), Action::SelectLine => todo!(), -- cgit From f7d66899f1ae087a87be5d084ec1ee9a03dd4ecc Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 14 Sep 2023 15:20:23 +0200 Subject: Implement `Action::SelectWord` in `text::Editor` --- graphics/Cargo.toml | 1 + graphics/src/text/editor.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 26bd1435..3165810b 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -34,6 +34,7 @@ raw-window-handle.workspace = true rustc-hash.workspace = true thiserror.workspace = true twox-hash.workspace = true +unicode-segmentation.workspace = true image.workspace = true image.optional = true diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c6b2abd5..3fd2c4fe 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -274,7 +274,66 @@ impl editor::Editor for Editor { } } } - Action::SelectWord => todo!(), + Action::SelectWord => { + use unicode_segmentation::UnicodeSegmentation; + + let cursor = editor.cursor(); + + if let Some(line) = editor.buffer().lines.get(cursor.line) { + let (start, end) = + UnicodeSegmentation::unicode_word_indices(line.text()) + // Split words with dots + .flat_map(|(i, word)| { + word.split('.').scan(i, |current, word| { + let start = *current; + *current += word.len() + 1; + + Some((start, word)) + }) + }) + // Turn words into ranges + .map(|(i, word)| (i, i + word.len())) + // Find the word at cursor + .find(|&(start, end)| { + start <= cursor.index && cursor.index < end + }) + // Cursor is not in a word. Let's select its punctuation cluster. + .unwrap_or_else(|| { + let start = line.text()[..cursor.index] + .char_indices() + .rev() + .take_while(|(_, c)| { + c.is_ascii_punctuation() + }) + .map(|(i, _)| i) + .last() + .unwrap_or(cursor.index); + + let end = line.text()[cursor.index..] + .char_indices() + .skip_while(|(_, c)| { + c.is_ascii_punctuation() + }) + .map(|(i, _)| i + cursor.index) + .next() + .unwrap_or(cursor.index); + + (start, end) + }); + + if start != end { + editor.set_cursor(cosmic_text::Cursor { + index: start, + ..cursor + }); + + editor.set_select_opt(Some(cosmic_text::Cursor { + index: end, + ..cursor + })); + } + } + } Action::SelectLine => todo!(), // Editing events -- cgit From 8cad1d682a306071f1f03bff4e70196adc946491 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 14 Sep 2023 15:23:20 +0200 Subject: Implement `Action::SelectLine` in `text::Editor` --- graphics/src/text/editor.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 3fd2c4fe..8eec94c9 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -334,7 +334,24 @@ impl editor::Editor for Editor { } } } - Action::SelectLine => todo!(), + Action::SelectLine => { + let cursor = editor.cursor(); + + if let Some(line_length) = editor + .buffer() + .lines + .get(cursor.line) + .map(|line| line.text().len()) + { + editor + .set_cursor(cosmic_text::Cursor { index: 0, ..cursor }); + + editor.set_select_opt(Some(cosmic_text::Cursor { + index: line_length, + ..cursor + })); + } + } // Editing events Action::Insert(c) => { -- cgit From c7d02e24e6f8265c205a68bd97b2643d40ae30ee Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 14 Sep 2023 18:57:09 +0200 Subject: Remove `Editor::min_bounds` and use `bounds` instead --- core/src/renderer/null.rs | 4 ---- core/src/text/editor.rs | 14 -------------- graphics/src/text/editor.rs | 4 ---- tiny_skia/src/text.rs | 2 +- wgpu/src/text.rs | 2 +- 5 files changed, 2 insertions(+), 24 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index adf75969..e714e492 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -131,10 +131,6 @@ impl text::Editor for () { Size::ZERO } - fn min_bounds(&self) -> Size { - Size::ZERO - } - fn update( &mut self, _new_bounds: Size, diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 3adfc61a..56cda3ef 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -14,10 +14,6 @@ pub trait Editor: Sized + Default { /// Returns the current boundaries of the [`Editor`]. fn bounds(&self) -> Size; - /// Returns the minimum boundaries that can fit the contents of the - /// [`Editor`]. - fn min_bounds(&self) -> Size; - /// Updates the [`Editor`] with some new attributes. fn update( &mut self, @@ -26,16 +22,6 @@ pub trait Editor: Sized + Default { new_size: Pixels, new_line_height: LineHeight, ); - - /// Returns the minimum width that can fit the contents of the [`Editor`]. - fn min_width(&self) -> f32 { - self.min_bounds().width - } - - /// Returns the minimum height that can fit the contents of the [`Editor`]. - fn min_height(&self) -> f32 { - self.min_bounds().height - } } #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 8eec94c9..6d9e9bb6 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -410,10 +410,6 @@ impl editor::Editor for Editor { self.internal().bounds } - fn min_bounds(&self) -> Size { - self.internal().min_bounds - } - fn update( &mut self, new_bounds: Size, diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index d055c749..96cfbf32 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -86,7 +86,7 @@ impl Pipeline { font_system.raw(), &mut self.glyph_cache, editor.buffer(), - Rectangle::new(position, editor.min_bounds()), + Rectangle::new(position, editor.bounds()), color, alignment::Horizontal::Left, alignment::Vertical::Top, diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 397c38dd..581df0cb 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -151,7 +151,7 @@ impl Pipeline { ( editor.buffer(), - Rectangle::new(*position, editor.min_bounds()), + Rectangle::new(*position, editor.bounds()), alignment::Horizontal::Left, alignment::Vertical::Top, *color, -- cgit From 3afac11784b9cedc7e6208e3bf1d0365e1f5e902 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 14 Sep 2023 18:58:52 +0200 Subject: Remove `min_bounds` field in `graphics::text::Editor` --- graphics/src/text/editor.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 6d9e9bb6..07a2d72a 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -15,7 +15,6 @@ struct Internal { editor: cosmic_text::Editor, font: Font, bounds: Size, - min_bounds: Size, version: text::Version, } @@ -426,14 +425,10 @@ impl editor::Editor for Editor { let mut font_system = text::font_system().write().expect("Write font system"); - let mut changed = false; - if font_system.version() != internal.version { for line in internal.editor.buffer_mut().lines.iter_mut() { line.reset(); } - - changed = true; } if new_font != internal.font { @@ -442,8 +437,6 @@ impl editor::Editor for Editor { text::to_attributes(new_font), )); } - - changed = true; } let metrics = internal.editor.buffer().metrics(); @@ -456,8 +449,6 @@ impl editor::Editor for Editor { font_system.raw(), cosmic_text::Metrics::new(new_size.0, new_line_height.0), ); - - changed = true; } if new_bounds != internal.bounds { @@ -468,11 +459,6 @@ impl editor::Editor for Editor { ); internal.bounds = new_bounds; - changed = true; - } - - if changed { - internal.min_bounds = text::measure(internal.editor.buffer()); } self.0 = Some(Arc::new(internal)); @@ -489,7 +475,6 @@ impl PartialEq for Internal { fn eq(&self, other: &Self) -> bool { self.font == other.font && self.bounds == other.bounds - && self.min_bounds == other.min_bounds && self.editor.buffer().metrics() == other.editor.buffer().metrics() } } @@ -505,7 +490,6 @@ impl Default for Internal { )), font: Font::default(), bounds: Size::ZERO, - min_bounds: Size::ZERO, version: text::Version::default(), } } @@ -516,7 +500,6 @@ impl fmt::Debug for Internal { f.debug_struct("Internal") .field("font", &self.font) .field("bounds", &self.bounds) - .field("min_bounds", &self.min_bounds) .finish() } } -- cgit From 8e6e37e0cee79a2f293abedd18a6a7249575bb63 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 14 Sep 2023 19:05:50 +0200 Subject: Fix broken intra-doc links --- core/src/text.rs | 2 ++ widget/src/lib.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/core/src/text.rs b/core/src/text.rs index 5aacbcc5..90581fea 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -137,6 +137,8 @@ impl Hit { /// /// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some /// [`Text`]. +/// +/// [`compare`]: Paragraph::compare #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Difference { /// No difference. diff --git a/widget/src/lib.rs b/widget/src/lib.rs index f8e5e865..4c318d75 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -93,6 +93,8 @@ pub use space::Space; #[doc(no_inline)] pub use text::Text; #[doc(no_inline)] +pub use text_editor::TextEditor; +#[doc(no_inline)] pub use text_input::TextInput; #[doc(no_inline)] pub use toggler::Toggler; -- cgit From f7fc13d98c52a9260b1ab55394a0c3d2693318ed Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 14 Sep 2023 22:55:54 +0200 Subject: Fix `Copy` action being triggered without any modifiers --- widget/src/text_editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 48de6409..114d35ef 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -421,7 +421,9 @@ impl Update { keyboard::KeyCode::Backspace => edit(Action::Backspace), keyboard::KeyCode::Delete => edit(Action::Delete), keyboard::KeyCode::Escape => Some(Self::Unfocus), - keyboard::KeyCode::C => Some(Self::Copy), + keyboard::KeyCode::C if modifiers.command() => { + Some(Self::Copy) + } keyboard::KeyCode::V if modifiers.command() && !modifiers.alt() => { -- cgit From c6d0443627c22dcf1576303e5a426aa3622f1b7d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 16 Sep 2023 15:27:25 +0200 Subject: Implement methods to query the contents of a `TextEditor` --- core/src/renderer/null.rs | 12 ++++++++++++ core/src/text/editor.rs | 6 ++++++ graphics/src/text/editor.rs | 41 ++++++++++++++++++++++++++++++++++++++ widget/src/text_editor.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index e714e492..01a52c7a 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -125,6 +125,18 @@ impl text::Editor for () { text::editor::Cursor::Caret(Point::ORIGIN) } + fn selection(&self) -> Option { + None + } + + fn line(&self, _index: usize) -> Option<&str> { + None + } + + fn line_count(&self) -> usize { + 0 + } + fn perform(&mut self, _action: text::editor::Action) {} fn bounds(&self) -> Size { diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 56cda3ef..5532fac5 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -9,6 +9,12 @@ pub trait Editor: Sized + Default { fn cursor(&self) -> Cursor; + fn selection(&self) -> Option; + + fn line(&self, index: usize) -> Option<&str>; + + fn line_count(&self) -> usize; + fn perform(&mut self, action: Action); /// Returns the current boundaries of the [`Editor`]. diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 07a2d72a..1e375a25 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -69,6 +69,47 @@ impl editor::Editor for Editor { }))) } + fn line(&self, index: usize) -> Option<&str> { + self.buffer() + .lines + .get(index) + .map(cosmic_text::BufferLine::text) + } + + fn line_count(&self) -> usize { + self.buffer().lines.len() + } + + fn selection(&self) -> Option { + let internal = self.internal(); + + let cursor = internal.editor.cursor(); + let selection = internal.editor.select_opt()?; + + let (start, end) = if cursor < selection { + (cursor, selection) + } else { + (selection, cursor) + }; + + Some( + internal.editor.buffer().lines[start.line..=end.line] + .iter() + .enumerate() + .map(|(i, line)| { + if i == 0 { + &line.text()[start.index..] + } else if i == end.line - start.line { + &line.text()[..end.index] + } else { + line.text() + } + }) + .collect::>() + .join("\n"), + ) + } + fn cursor(&self) -> editor::Cursor { let internal = self.internal(); diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 114d35ef..ec7a6d1d 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -100,6 +100,54 @@ where internal.editor.perform(action); internal.is_dirty = true; } + + pub fn line_count(&self) -> usize { + self.0.borrow().editor.line_count() + } + + pub fn line( + &self, + index: usize, + ) -> Option + '_> { + std::cell::Ref::filter_map(self.0.borrow(), |internal| { + internal.editor.line(index) + }) + .ok() + } + + pub fn lines( + &self, + ) -> impl Iterator + '_> { + struct Lines<'a, Renderer: text::Renderer> { + internal: std::cell::Ref<'a, Internal>, + current: usize, + } + + impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> { + type Item = std::cell::Ref<'a, str>; + + fn next(&mut self) -> Option { + let line = std::cell::Ref::filter_map( + std::cell::Ref::clone(&self.internal), + |internal| internal.editor.line(self.current), + ) + .ok()?; + + self.current += 1; + + Some(line) + } + } + + Lines { + internal: self.0.borrow(), + current: 0, + } + } + + pub fn selection(&self) -> Option { + self.0.borrow().editor.selection() + } } impl Default for Content -- cgit From d051f21597bb333ac10183aaa3214a292e9aa365 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 16 Sep 2023 15:40:16 +0200 Subject: Implement `Copy` and `Paste` actions for `text::Editor` --- core/src/text/editor.rs | 5 ++++- examples/editor/src/main.rs | 2 +- graphics/src/text/editor.rs | 11 +++++++++++ widget/src/text_editor.rs | 11 ++++++++--- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 5532fac5..003557c1 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -1,6 +1,8 @@ use crate::text::LineHeight; use crate::{Pixels, Point, Rectangle, Size}; +use std::sync::Arc; + pub trait Editor: Sized + Default { type Font: Copy + PartialEq + Default; @@ -30,13 +32,14 @@ pub trait Editor: Sized + Default { ); } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum Action { Move(Motion), Select(Motion), SelectWord, SelectLine, Insert(char), + Paste(Arc), Enter, Backspace, Delete, diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 2a70b34c..11819c69 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -9,7 +9,7 @@ struct Editor { content: text_editor::Content, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), } diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 1e375a25..1890cb82 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -398,6 +398,17 @@ impl editor::Editor for Editor { editor .action(font_system.raw(), cosmic_text::Action::Insert(c)); } + Action::Paste(text) => { + editor.insert_string(&text, None); + + // TODO: Fix cosmic-text + // Cursor should be marked as moved after `insert_string`. + let cursor = editor.cursor(); + + editor + .buffer_mut() + .shape_until_cursor(font_system.raw(), cursor); + } Action::Enter => { editor.action(font_system.raw(), cosmic_text::Action::Enter); } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index ec7a6d1d..0bb6b7d3 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -12,6 +12,7 @@ use crate::core::{ }; use std::cell::RefCell; +use std::sync::Arc; pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Motion}; @@ -253,10 +254,14 @@ where Update::Edit(action) => { shell.publish(on_edit(action)); } - Update::Copy => todo!(), + Update::Copy => { + if let Some(selection) = self.content.selection() { + clipboard.write(selection); + } + } Update::Paste => { - if let Some(_contents) = clipboard.read() { - todo!() + if let Some(contents) = clipboard.read() { + shell.publish(on_edit(Action::Paste(Arc::new(contents)))); } } } -- cgit From c9dbccba468da683af2513535c40374da804aa60 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 16 Sep 2023 16:27:02 +0200 Subject: Use fork of `cosmic-text` with some minor fixes --- Cargo.toml | 4 ++++ graphics/src/text/editor.rs | 36 +----------------------------------- 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af74a3cf..f8dd5f14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,3 +151,7 @@ wgpu = "0.17" winapi = "0.3" window_clipboard = "0.3" winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false } + +[patch.crates-io.cosmic-text] +git = "https://github.com/hecrj/cosmic-text.git" +rev = "cb83458e7d0b84ef37c5beb72dda5046d7d343a6" diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 1890cb82..a828a3bc 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -81,33 +81,7 @@ impl editor::Editor for Editor { } fn selection(&self) -> Option { - let internal = self.internal(); - - let cursor = internal.editor.cursor(); - let selection = internal.editor.select_opt()?; - - let (start, end) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; - - Some( - internal.editor.buffer().lines[start.line..=end.line] - .iter() - .enumerate() - .map(|(i, line)| { - if i == 0 { - &line.text()[start.index..] - } else if i == end.line - start.line { - &line.text()[..end.index] - } else { - line.text() - } - }) - .collect::>() - .join("\n"), - ) + self.internal().editor.copy_selection() } fn cursor(&self) -> editor::Cursor { @@ -400,14 +374,6 @@ impl editor::Editor for Editor { } Action::Paste(text) => { editor.insert_string(&text, None); - - // TODO: Fix cosmic-text - // Cursor should be marked as moved after `insert_string`. - let cursor = editor.cursor(); - - editor - .buffer_mut() - .shape_until_cursor(font_system.raw(), cursor); } Action::Enter => { editor.action(font_system.raw(), cosmic_text::Action::Enter); -- cgit From 45c5cfe5774ac99a6e1b1d1014418f68b21b41cf Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 16 Sep 2023 19:05:31 +0200 Subject: Avoid drag on double or triple click for now in `TextEditor` --- core/src/mouse/click.rs | 4 ++++ widget/src/text_editor.rs | 52 +++++++++++++++++++++++++---------------------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 4a7d796c..e8e5fb56 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -61,6 +61,10 @@ impl Click { self.kind } + pub fn position(&self) -> Point { + self.position + } + fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { let duration = if time > self.time { Some(time - self.time) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 0bb6b7d3..68e3c656 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -162,8 +162,8 @@ where struct State { is_focused: bool, - is_dragging: bool, last_click: Option, + drag_click: Option, } impl<'a, Message, Renderer> Widget @@ -179,8 +179,8 @@ where fn state(&self) -> widget::tree::State { widget::tree::State::new(State { is_focused: false, - is_dragging: false, last_click: None, + drag_click: None, }) } @@ -238,18 +238,27 @@ where }; match update { - Update::Click { click, action } => { + Update::Click(click) => { + let action = match click.kind() { + mouse::click::Kind::Single => { + Action::Click(click.position()) + } + mouse::click::Kind::Double => Action::SelectWord, + mouse::click::Kind::Triple => Action::SelectLine, + }; + state.is_focused = true; - state.is_dragging = true; state.last_click = Some(click); + state.drag_click = Some(click.kind()); + shell.publish(on_edit(action)); } Update::Unfocus => { state.is_focused = false; - state.is_dragging = false; + state.drag_click = None; } - Update::StopDragging => { - state.is_dragging = false; + Update::Release => { + state.drag_click = None; } Update::Edit(action) => { shell.publish(on_edit(action)); @@ -393,9 +402,9 @@ where } enum Update { - Click { click: mouse::Click, action: Action }, + Click(mouse::Click), Unfocus, - StopDragging, + Release, Edit(Action), Copy, Paste, @@ -423,15 +432,7 @@ impl Update { state.last_click, ); - let action = match click.kind() { - mouse::click::Kind::Single => { - Action::Click(cursor_position) - } - mouse::click::Kind::Double => Action::SelectWord, - mouse::click::Kind::Triple => Action::SelectLine, - }; - - Some(Update::Click { click, action }) + Some(Update::Click(click)) } else if state.is_focused { Some(Update::Unfocus) } else { @@ -439,14 +440,17 @@ impl Update { } } mouse::Event::ButtonReleased(mouse::Button::Left) => { - Some(Update::StopDragging) + Some(Update::Release) } - mouse::Event::CursorMoved { .. } if state.is_dragging => { - let cursor_position = cursor.position_in(bounds)? - - Vector::new(padding.top, padding.left); + mouse::Event::CursorMoved { .. } => match state.drag_click { + Some(mouse::click::Kind::Single) => { + let cursor_position = cursor.position_in(bounds)? + - Vector::new(padding.top, padding.left); - edit(Action::Drag(cursor_position)) - } + edit(Action::Drag(cursor_position)) + } + _ => None, + }, _ => None, }, Event::Keyboard(event) => match event { -- cgit From 723111bb0df486bffaedcaed0722b1793d65bfe3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 16 Sep 2023 19:09:31 +0200 Subject: Remove unnecessary `into_iter` call in `graphics::text` --- graphics/src/text.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 280e4f01..b4aeb2be 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -21,12 +21,11 @@ pub fn font_system() -> &'static RwLock { FONT_SYSTEM.get_or_init(|| { RwLock::new(FontSystem { - raw: cosmic_text::FontSystem::new_with_fonts( - [cosmic_text::fontdb::Source::Binary(Arc::new( + raw: cosmic_text::FontSystem::new_with_fonts([ + cosmic_text::fontdb::Source::Binary(Arc::new( include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), - ))] - .into_iter(), - ), + )), + ]), version: Version::default(), }) }) -- cgit From 76dc82e8e8b5201ec10f8d00d851c1decf998583 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 15:29:14 +0200 Subject: Draft `Highlighter` API --- core/src/renderer/null.rs | 11 ++++++++ core/src/text.rs | 2 ++ core/src/text/editor.rs | 9 ++++++ core/src/text/highlighter.rs | 56 ++++++++++++++++++++++++++++++++++++ graphics/src/text.rs | 8 +++++- graphics/src/text/editor.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++ style/src/lib.rs | 2 +- style/src/text_editor.rs | 16 ++++++++++- widget/src/helpers.rs | 2 +- widget/src/text_editor.rs | 64 ++++++++++++++++++++++++++++++++---------- 10 files changed, 218 insertions(+), 19 deletions(-) create mode 100644 core/src/text/highlighter.rs diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 01a52c7a..21597c8e 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -149,6 +149,17 @@ impl text::Editor for () { _new_font: Self::Font, _new_size: Pixels, _new_line_height: text::LineHeight, + _new_highlighter: &mut impl text::Highlighter, + ) { + } + + fn highlight( + &mut self, + _font: Self::Font, + _highlighter: &mut H, + _format_highlight: impl Fn( + &H::Highlight, + ) -> text::highlighter::Format, ) { } } diff --git a/core/src/text.rs b/core/src/text.rs index 90581fea..9b9c753c 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -2,8 +2,10 @@ mod paragraph; pub mod editor; +pub mod highlighter; pub use editor::Editor; +pub use highlighter::Highlighter; pub use paragraph::Paragraph; use crate::alignment; diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 003557c1..0f439c8d 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -1,3 +1,4 @@ +use crate::text::highlighter::{self, Highlighter}; use crate::text::LineHeight; use crate::{Pixels, Point, Rectangle, Size}; @@ -29,6 +30,14 @@ pub trait Editor: Sized + Default { new_font: Self::Font, new_size: Pixels, new_line_height: LineHeight, + new_highlighter: &mut impl Highlighter, + ); + + fn highlight( + &mut self, + font: Self::Font, + highlighter: &mut H, + format_highlight: impl Fn(&H::Highlight) -> highlighter::Format, ); } diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs new file mode 100644 index 00000000..1f9ac840 --- /dev/null +++ b/core/src/text/highlighter.rs @@ -0,0 +1,56 @@ +use crate::Color; + +use std::hash::Hash; +use std::ops::Range; + +pub trait Highlighter: Clone + 'static { + type Settings: Hash; + type Highlight; + + type Iterator<'a>: Iterator, Self::Highlight)> + where + Self: 'a; + + fn new(settings: &Self::Settings) -> Self; + + fn change_line(&mut self, line: usize); + + fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>; + + fn current_line(&self) -> usize; +} + +#[derive(Debug, Clone, Copy)] +pub struct Style { + pub color: Color, +} + +#[derive(Debug, Clone, Copy)] +pub struct PlainText; + +impl Highlighter for PlainText { + type Settings = (); + type Highlight = (); + + type Iterator<'a> = std::iter::Empty<(Range, Self::Highlight)>; + + fn new(_settings: &Self::Settings) -> Self { + Self + } + + fn change_line(&mut self, _line: usize) {} + + fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> { + std::iter::empty() + } + + fn current_line(&self) -> usize { + usize::MAX + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Format { + pub color: Option, + pub font: Option, +} diff --git a/graphics/src/text.rs b/graphics/src/text.rs index b4aeb2be..5fcfc699 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -10,7 +10,7 @@ pub use cosmic_text; use crate::core::font::{self, Font}; use crate::core::text::Shaping; -use crate::core::Size; +use crate::core::{Color, Size}; use once_cell::sync::OnceCell; use std::borrow::Cow; @@ -129,3 +129,9 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { Shaping::Advanced => cosmic_text::Shaping::Advanced, } } + +pub fn to_color(color: Color) -> cosmic_text::Color { + let [r, g, b, a] = color.into_rgba8(); + + cosmic_text::Color::rgba(r, g, b, a) +} diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index a828a3bc..901b4295 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,4 +1,5 @@ use crate::core::text::editor::{self, Action, Cursor, Direction, Motion}; +use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::LineHeight; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; @@ -15,6 +16,7 @@ struct Internal { editor: cosmic_text::Editor, font: Font, bounds: Size, + topmost_line_changed: Option, version: text::Version, } @@ -433,6 +435,7 @@ impl editor::Editor for Editor { new_font: Font, new_size: Pixels, new_line_height: LineHeight, + new_highlighter: &mut impl Highlighter, ) { let editor = self.0.take().expect("editor should always be initialized"); @@ -479,6 +482,69 @@ impl editor::Editor for Editor { internal.bounds = new_bounds; } + if let Some(topmost_line_changed) = internal.topmost_line_changed.take() + { + new_highlighter.change_line(topmost_line_changed); + } + + self.0 = Some(Arc::new(internal)); + } + + fn highlight( + &mut self, + font: Self::Font, + highlighter: &mut H, + format_highlight: impl Fn(&H::Highlight) -> highlighter::Format, + ) { + let internal = self.internal(); + + let scroll = internal.editor.buffer().scroll(); + let visible_lines = internal.editor.buffer().visible_lines(); + let last_visible_line = (scroll + visible_lines - 1) as usize; + + let current_line = highlighter.current_line(); + + if current_line > last_visible_line { + return; + } + + let editor = + self.0.take().expect("editor should always be initialized"); + + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let attributes = text::to_attributes(font); + + for line in &mut internal.editor.buffer_mut().lines + [current_line..=last_visible_line] + { + let mut list = cosmic_text::AttrsList::new(attributes); + + for (range, highlight) in highlighter.highlight_line(line.text()) { + let format = format_highlight(&highlight); + + list.add_span( + range, + cosmic_text::Attrs { + color_opt: format.color.map(text::to_color), + ..if let Some(font) = format.font { + text::to_attributes(font) + } else { + attributes + } + }, + ); + } + + let _ = line.set_attrs_list(list); + } + + internal.editor.shape_as_needed(font_system.raw()); + self.0 = Some(Arc::new(internal)); } } @@ -508,6 +574,7 @@ impl Default for Internal { )), font: Font::default(), bounds: Size::ZERO, + topmost_line_changed: None, version: text::Version::default(), } } diff --git a/style/src/lib.rs b/style/src/lib.rs index 7a97ac77..c9879f24 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -15,7 +15,7 @@ clippy::needless_borrow, clippy::new_without_default, clippy::useless_conversion, - missing_docs, + // missing_docs, unused_results, rustdoc::broken_intra_doc_links )] diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs index 45c9bad8..f1c31287 100644 --- a/style/src/text_editor.rs +++ b/style/src/text_editor.rs @@ -1,5 +1,6 @@ //! Change the appearance of a text editor. -use iced_core::{Background, BorderRadius, Color}; +use crate::core::text::highlighter; +use crate::core::{self, Background, BorderRadius, Color}; /// The appearance of a text input. #[derive(Debug, Clone, Copy)] @@ -45,3 +46,16 @@ pub trait StyleSheet { /// Produces the style of a disabled text input. fn disabled(&self, style: &Self::Style) -> Appearance; } + +pub trait Highlight { + fn format(&self, theme: &Theme) -> highlighter::Format; +} + +impl Highlight for () { + fn format(&self, _theme: &Theme) -> highlighter::Format { + highlighter::Format { + color: None, + font: None, + } + } +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index e3f31513..e0b58722 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -212,7 +212,7 @@ where /// [`TextEditor`]: crate::TextEditor pub fn text_editor( content: &text_editor::Content, -) -> TextEditor<'_, Message, Renderer> +) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Renderer> where Message: Clone, Renderer: core::text::Renderer, diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 68e3c656..b17e1156 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -4,6 +4,7 @@ use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::text::editor::{Cursor, Editor as _}; +use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{self, LineHeight}; use crate::core::widget::{self, Widget}; use crate::core::{ @@ -12,13 +13,15 @@ use crate::core::{ }; use std::cell::RefCell; +use std::ops::DerefMut; use std::sync::Arc; -pub use crate::style::text_editor::{Appearance, StyleSheet}; +pub use crate::style::text_editor::{Appearance, Highlight, StyleSheet}; pub use text::editor::{Action, Motion}; -pub struct TextEditor<'a, Message, Renderer = crate::Renderer> +pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> where + Highlighter: text::Highlighter, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { @@ -31,9 +34,11 @@ where padding: Padding, style: ::Style, on_edit: Option Message + 'a>>, + highlighter_settings: Highlighter::Settings, } -impl<'a, Message, Renderer> TextEditor<'a, Message, Renderer> +impl<'a, Message, Renderer> + TextEditor<'a, highlighter::PlainText, Message, Renderer> where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -49,9 +54,19 @@ where padding: Padding::new(5.0), style: Default::default(), on_edit: None, + highlighter_settings: (), } } +} +impl<'a, Highlighter, Message, Renderer> + TextEditor<'a, Highlighter, Message, Renderer> +where + Highlighter: text::Highlighter, + Highlighter::Highlight: Highlight, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ pub fn on_edit(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self { self.on_edit = Some(Box::new(on_edit)); self @@ -160,20 +175,23 @@ where } } -struct State { +struct State { is_focused: bool, last_click: Option, drag_click: Option, + highlighter: RefCell, } -impl<'a, Message, Renderer> Widget - for TextEditor<'a, Message, Renderer> +impl<'a, Highlighter, Message, Renderer> Widget + for TextEditor<'a, Highlighter, Message, Renderer> where + Highlighter: text::Highlighter, + Highlighter::Highlight: Highlight, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { fn tag(&self) -> widget::tree::Tag { - widget::tree::Tag::of::() + widget::tree::Tag::of::>() } fn state(&self) -> widget::tree::State { @@ -181,6 +199,9 @@ where is_focused: false, last_click: None, drag_click: None, + highlighter: RefCell::new(Highlighter::new( + &self.highlighter_settings, + )), }) } @@ -194,17 +215,19 @@ where fn layout( &self, - _tree: &mut widget::Tree, + tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> iced_renderer::core::layout::Node { let mut internal = self.content.0.borrow_mut(); + let state = tree.state.downcast_mut::>(); internal.editor.update( limits.pad(self.padding).max(), self.font.unwrap_or_else(|| renderer.default_font()), self.text_size.unwrap_or_else(|| renderer.default_size()), self.line_height, + state.highlighter.borrow_mut().deref_mut(), ); layout::Node::new(limits.max()) @@ -225,7 +248,7 @@ where return event::Status::Ignored; }; - let state = tree.state.downcast_mut::(); + let state = tree.state.downcast_mut::>(); let Some(update) = Update::from_event( event, @@ -290,8 +313,14 @@ where ) { let bounds = layout.bounds(); - let internal = self.content.0.borrow(); - let state = tree.state.downcast_ref::(); + let mut internal = self.content.0.borrow_mut(); + let state = tree.state.downcast_ref::>(); + + internal.editor.highlight( + self.font.unwrap_or_else(|| renderer.default_font()), + state.highlighter.borrow_mut().deref_mut(), + |highlight| highlight.format(theme), + ); let is_disabled = self.on_edit.is_none(); let is_mouse_over = cursor.is_over(bounds); @@ -389,14 +418,19 @@ where } } -impl<'a, Message, Renderer> From> +impl<'a, Highlighter, Message, Renderer> + From> for Element<'a, Message, Renderer> where + Highlighter: text::Highlighter, + Highlighter::Highlight: Highlight, Message: 'a, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - fn from(text_editor: TextEditor<'a, Message, Renderer>) -> Self { + fn from( + text_editor: TextEditor<'a, Highlighter, Message, Renderer>, + ) -> Self { Self::new(text_editor) } } @@ -411,9 +445,9 @@ enum Update { } impl Update { - fn from_event( + fn from_event( event: Event, - state: &State, + state: &State, bounds: Rectangle, padding: Padding, cursor: mouse::Cursor, -- cgit From d3011992a76e83e12f74402c2ade616cdc7f1497 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 19:03:58 +0200 Subject: Implement basic syntax highlighting with `syntect` in `editor` example --- core/src/text/highlighter.rs | 2 +- examples/editor/Cargo.toml | 4 +- examples/editor/src/main.rs | 168 ++++++++++++++++++++++++++++++++++++++++++- graphics/src/text/editor.rs | 25 ++++++- widget/src/text_editor.rs | 18 +++++ 5 files changed, 211 insertions(+), 6 deletions(-) diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs index 1f9ac840..a929826f 100644 --- a/core/src/text/highlighter.rs +++ b/core/src/text/highlighter.rs @@ -3,7 +3,7 @@ use crate::Color; use std::hash::Hash; use std::ops::Range; -pub trait Highlighter: Clone + 'static { +pub trait Highlighter: 'static { type Settings: Hash; type Highlight; diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index 528cf23c..930ee592 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,4 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] \ No newline at end of file +iced.features = ["advanced", "debug"] + +syntect = "5.1" \ No newline at end of file diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 11819c69..a72feebc 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,6 +1,8 @@ use iced::widget::{container, text_editor}; use iced::{Element, Font, Sandbox, Settings, Theme}; +use highlighter::Highlighter; + pub fn main() -> iced::Result { Editor::run(Settings::default()) } @@ -41,7 +43,10 @@ impl Sandbox for Editor { container( text_editor(&self.content) .on_edit(Message::Edit) - .font(Font::with_name("Hasklug Nerd Font Mono")), + .font(Font::with_name("Hasklug Nerd Font Mono")) + .highlight::(highlighter::Settings { + token: String::from("md"), + }), ) .padding(20) .into() @@ -51,3 +56,164 @@ impl Sandbox for Editor { Theme::Dark } } + +mod highlighter { + use iced::advanced::text::highlighter; + use iced::widget::text_editor; + use iced::{Color, Font, Theme}; + + use std::ops::Range; + use syntect::highlighting; + use syntect::parsing; + + #[derive(Debug, Clone, Hash)] + pub struct Settings { + pub token: String, + } + + pub struct Highlight(highlighting::StyleModifier); + + impl text_editor::Highlight for Highlight { + fn format(&self, _theme: &Theme) -> highlighter::Format { + highlighter::Format { + color: self.0.foreground.map(|color| { + Color::from_rgba8( + color.r, + color.g, + color.b, + color.a as f32 / 255.0, + ) + }), + font: None, + } + } + } + + pub struct Highlighter { + syntaxes: parsing::SyntaxSet, + parser: parsing::ParseState, + stack: parsing::ScopeStack, + theme: highlighting::Theme, + token: String, + current_line: usize, + } + + impl highlighter::Highlighter for Highlighter { + type Settings = Settings; + type Highlight = Highlight; + + type Iterator<'a> = + Box, Self::Highlight)> + 'a>; + + fn new(settings: &Self::Settings) -> Self { + let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); + + let syntax = syntaxes + .find_syntax_by_token(&settings.token) + .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); + + let parser = parsing::ParseState::new(&syntax); + let stack = parsing::ScopeStack::new(); + + let theme = highlighting::ThemeSet::load_defaults() + .themes + .remove("base16-mocha.dark") + .unwrap(); + + Highlighter { + syntaxes, + parser, + stack, + theme, + token: settings.token.clone(), + current_line: 0, + } + } + + fn change_line(&mut self, _line: usize) { + // TODO: Caching + let syntax = self + .syntaxes + .find_syntax_by_token(&self.token) + .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()); + + self.parser = parsing::ParseState::new(&syntax); + self.stack = parsing::ScopeStack::new(); + self.current_line = 0; + } + + fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { + self.current_line += 1; + + let ops = self + .parser + .parse_line(line, &self.syntaxes) + .unwrap_or_default(); + + Box::new( + ScopeRangeIterator { + ops, + line_length: line.len(), + index: 0, + last_str_index: 0, + } + .filter_map(move |(range, scope)| { + let highlighter = + highlighting::Highlighter::new(&self.theme); + let _ = self.stack.apply(&scope); + + if range.is_empty() { + None + } else { + Some(( + range, + Highlight( + highlighter + .style_mod_for_stack(&self.stack.scopes), + ), + )) + } + }), + ) + } + + fn current_line(&self) -> usize { + self.current_line + } + } + + pub struct ScopeRangeIterator { + ops: Vec<(usize, parsing::ScopeStackOp)>, + line_length: usize, + index: usize, + last_str_index: usize, + } + + impl Iterator for ScopeRangeIterator { + type Item = (std::ops::Range, parsing::ScopeStackOp); + + fn next(&mut self) -> Option { + if self.index > self.ops.len() { + return None; + } + + let next_str_i = if self.index == self.ops.len() { + self.line_length + } else { + self.ops[self.index].0 + }; + + let range = self.last_str_index..next_str_i; + self.last_str_index = next_str_i; + + let op = if self.index == 0 { + parsing::ScopeStackOp::Noop + } else { + self.ops[self.index - 1].1.clone() + }; + + self.index += 1; + Some((range, op)) + } + } +} diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 901b4295..58fcc3dc 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -447,17 +447,26 @@ impl editor::Editor for Editor { text::font_system().write().expect("Write font system"); if font_system.version() != internal.version { + log::trace!("Updating `FontSystem` of `Editor`..."); + for line in internal.editor.buffer_mut().lines.iter_mut() { line.reset(); } } if new_font != internal.font { + log::trace!("Updating font of `Editor`..."); + for line in internal.editor.buffer_mut().lines.iter_mut() { let _ = line.set_attrs_list(cosmic_text::AttrsList::new( text::to_attributes(new_font), )); } + + internal.font = new_font; + internal.topmost_line_changed = Some(0); + + internal.editor.shape_as_needed(font_system.raw()); } let metrics = internal.editor.buffer().metrics(); @@ -466,6 +475,8 @@ impl editor::Editor for Editor { if new_size.0 != metrics.font_size || new_line_height.0 != metrics.line_height { + log::trace!("Updating `Metrics` of `Editor`..."); + internal.editor.buffer_mut().set_metrics( font_system.raw(), cosmic_text::Metrics::new(new_size.0, new_line_height.0), @@ -473,6 +484,8 @@ impl editor::Editor for Editor { } if new_bounds != internal.bounds { + log::trace!("Updating size of `Editor`..."); + internal.editor.buffer_mut().set_size( font_system.raw(), new_bounds.width, @@ -484,6 +497,10 @@ impl editor::Editor for Editor { if let Some(topmost_line_changed) = internal.topmost_line_changed.take() { + log::trace!( + "Notifying highlighter of line change: {topmost_line_changed}" + ); + new_highlighter.change_line(topmost_line_changed); } @@ -497,10 +514,12 @@ impl editor::Editor for Editor { format_highlight: impl Fn(&H::Highlight) -> highlighter::Format, ) { let internal = self.internal(); + let buffer = internal.editor.buffer(); - let scroll = internal.editor.buffer().scroll(); - let visible_lines = internal.editor.buffer().visible_lines(); - let last_visible_line = (scroll + visible_lines - 1) as usize; + let scroll = buffer.scroll(); + let visible_lines = buffer.visible_lines(); + let last_visible_line = + ((scroll + visible_lines) as usize).min(buffer.lines.len()) - 1; let current_line = highlighter.current_line(); diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index b17e1156..03adbb59 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -81,6 +81,24 @@ where self.padding = padding.into(); self } + + pub fn highlight( + self, + settings: H::Settings, + ) -> TextEditor<'a, H, Message, Renderer> { + TextEditor { + content: self.content, + font: self.font, + text_size: self.text_size, + line_height: self.line_height, + width: self.width, + height: self.height, + padding: self.padding, + style: self.style, + on_edit: self.on_edit, + highlighter_settings: settings, + } + } } pub struct Content(RefCell>) -- cgit From 23d00445ff1225b3e5ca99cb27966143cda8a2ce Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 19:06:20 +0200 Subject: Use `saturating_sub` for `last_visible_line` in `text::Editor` --- graphics/src/text/editor.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 58fcc3dc..fbae287e 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -518,8 +518,9 @@ impl editor::Editor for Editor { let scroll = buffer.scroll(); let visible_lines = buffer.visible_lines(); - let last_visible_line = - ((scroll + visible_lines) as usize).min(buffer.lines.len()) - 1; + let last_visible_line = ((scroll + visible_lines) as usize) + .min(buffer.lines.len()) + .saturating_sub(1); let current_line = highlighter.current_line(); -- cgit From 2897986f2ded7318894a52572bec3d62754ebfaa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 19:27:51 +0200 Subject: Notify `Highlighter` of topmost line change --- core/src/text/editor.rs | 9 +++++-- graphics/src/text/editor.rs | 58 +++++++++++++++++++++++++++++++-------------- widget/src/text_editor.rs | 25 ++++++++++--------- 3 files changed, 61 insertions(+), 31 deletions(-) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 0f439c8d..2144715f 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -47,13 +47,18 @@ pub enum Action { Select(Motion), SelectWord, SelectLine, + Edit(Edit), + Click(Point), + Drag(Point), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Edit { Insert(char), Paste(Arc), Enter, Backspace, Delete, - Click(Point), - Drag(Point), } #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index fbae287e..47c210bd 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,10 +1,12 @@ -use crate::core::text::editor::{self, Action, Cursor, Direction, Motion}; +use crate::core::text::editor::{ + self, Action, Cursor, Direction, Edit, Motion, +}; use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::LineHeight; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; -use cosmic_text::Edit; +use cosmic_text::Edit as _; use std::fmt; use std::sync::{self, Arc}; @@ -370,22 +372,42 @@ impl editor::Editor for Editor { } // Editing events - Action::Insert(c) => { - editor - .action(font_system.raw(), cosmic_text::Action::Insert(c)); - } - Action::Paste(text) => { - editor.insert_string(&text, None); - } - Action::Enter => { - editor.action(font_system.raw(), cosmic_text::Action::Enter); - } - Action::Backspace => { - editor - .action(font_system.raw(), cosmic_text::Action::Backspace); - } - Action::Delete => { - editor.action(font_system.raw(), cosmic_text::Action::Delete); + Action::Edit(edit) => { + match edit { + Edit::Insert(c) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Insert(c), + ); + } + Edit::Paste(text) => { + editor.insert_string(&text, None); + } + Edit::Enter => { + editor.action( + font_system.raw(), + cosmic_text::Action::Enter, + ); + } + Edit::Backspace => { + editor.action( + font_system.raw(), + cosmic_text::Action::Backspace, + ); + } + Edit::Delete => { + editor.action( + font_system.raw(), + cosmic_text::Action::Delete, + ); + } + } + + let cursor = editor.cursor(); + let selection = editor.select_opt().unwrap_or(cursor); + + internal.topmost_line_changed = + Some(cursor.min(selection).line); } // Mouse events diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 03adbb59..c30e185f 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -17,7 +17,7 @@ use std::ops::DerefMut; use std::sync::Arc; pub use crate::style::text_editor::{Appearance, Highlight, StyleSheet}; -pub use text::editor::{Action, Motion}; +pub use text::editor::{Action, Edit, Motion}; pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> where @@ -301,7 +301,7 @@ where Update::Release => { state.drag_click = None; } - Update::Edit(action) => { + Update::Action(action) => { shell.publish(on_edit(action)); } Update::Copy => { @@ -311,7 +311,9 @@ where } Update::Paste => { if let Some(contents) = clipboard.read() { - shell.publish(on_edit(Action::Paste(Arc::new(contents)))); + shell.publish(on_edit(Action::Edit(Edit::Paste( + Arc::new(contents), + )))); } } } @@ -457,7 +459,7 @@ enum Update { Click(mouse::Click), Unfocus, Release, - Edit(Action), + Action(Action), Copy, Paste, } @@ -470,7 +472,8 @@ impl Update { padding: Padding, cursor: mouse::Cursor, ) -> Option { - let edit = |action| Some(Update::Edit(action)); + let action = |action| Some(Update::Action(action)); + let edit = |edit| action(Action::Edit(edit)); match event { Event::Mouse(event) => match event { @@ -499,7 +502,7 @@ impl Update { let cursor_position = cursor.position_in(bounds)? - Vector::new(padding.top, padding.left); - edit(Action::Drag(cursor_position)) + action(Action::Drag(cursor_position)) } _ => None, }, @@ -518,7 +521,7 @@ impl Update { motion }; - return edit(if modifiers.shift() { + return action(if modifiers.shift() { Action::Select(motion) } else { Action::Move(motion) @@ -526,9 +529,9 @@ impl Update { } match key_code { - keyboard::KeyCode::Enter => edit(Action::Enter), - keyboard::KeyCode::Backspace => edit(Action::Backspace), - keyboard::KeyCode::Delete => edit(Action::Delete), + keyboard::KeyCode::Enter => edit(Edit::Enter), + keyboard::KeyCode::Backspace => edit(Edit::Backspace), + keyboard::KeyCode::Delete => edit(Edit::Delete), keyboard::KeyCode::Escape => Some(Self::Unfocus), keyboard::KeyCode::C if modifiers.command() => { Some(Self::Copy) @@ -542,7 +545,7 @@ impl Update { } } keyboard::Event::CharacterReceived(c) if state.is_focused => { - edit(Action::Insert(c)) + edit(Edit::Insert(c)) } _ => None, }, -- cgit From 790c0dabcf0a50a2466e47daeb4f1e149b2ede5a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 21:45:13 +0200 Subject: Implement syntax highlighting cache in `editor` example --- Cargo.toml | 4 +++ examples/editor/src/main.rs | 67 ++++++++++++++++++++++++++++----------------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f8dd5f14..70f84460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,3 +155,7 @@ winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8 [patch.crates-io.cosmic-text] git = "https://github.com/hecrj/cosmic-text.git" rev = "cb83458e7d0b84ef37c5beb72dda5046d7d343a6" + +[patch.crates-io.rangemap] +git = "https://github.com/hecrj/rangemap.git" +branch = "fix/partial-eq" diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index a72feebc..1235d38b 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -64,7 +64,7 @@ mod highlighter { use std::ops::Range; use syntect::highlighting; - use syntect::parsing; + use syntect::parsing::{self, SyntaxReference}; #[derive(Debug, Clone, Hash)] pub struct Settings { @@ -91,13 +91,14 @@ mod highlighter { pub struct Highlighter { syntaxes: parsing::SyntaxSet, - parser: parsing::ParseState, - stack: parsing::ScopeStack, + syntax: SyntaxReference, + caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, theme: highlighting::Theme, - token: String, current_line: usize, } + const LINES_PER_SNAPSHOT: usize = 50; + impl highlighter::Highlighter for Highlighter { type Settings = Settings; type Highlight = Highlight; @@ -121,34 +122,53 @@ mod highlighter { .unwrap(); Highlighter { + syntax: syntax.clone(), syntaxes, - parser, - stack, + caches: vec![(parser, stack)], theme, - token: settings.token.clone(), current_line: 0, } } - fn change_line(&mut self, _line: usize) { - // TODO: Caching - let syntax = self - .syntaxes - .find_syntax_by_token(&self.token) - .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()); + fn change_line(&mut self, line: usize) { + let snapshot = line / LINES_PER_SNAPSHOT; + + if snapshot <= self.caches.len() { + self.caches.truncate(snapshot); + self.current_line = snapshot * LINES_PER_SNAPSHOT; + } else { + self.caches.truncate(1); + self.current_line = 0; + } + + let (parser, stack) = + self.caches.last().cloned().unwrap_or_else(|| { + ( + parsing::ParseState::new(&self.syntax), + parsing::ScopeStack::new(), + ) + }); - self.parser = parsing::ParseState::new(&syntax); - self.stack = parsing::ScopeStack::new(); - self.current_line = 0; + self.caches.push((parser, stack)); } fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { + if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() { + let (parser, stack) = + self.caches.last().expect("Caches must not be empty"); + + self.caches.push((parser.clone(), stack.clone())); + } + self.current_line += 1; - let ops = self - .parser - .parse_line(line, &self.syntaxes) - .unwrap_or_default(); + let (parser, stack) = + self.caches.last_mut().expect("Caches must not be empty"); + + let ops = + parser.parse_line(line, &self.syntaxes).unwrap_or_default(); + + let highlighter = highlighting::Highlighter::new(&self.theme); Box::new( ScopeRangeIterator { @@ -158,9 +178,7 @@ mod highlighter { last_str_index: 0, } .filter_map(move |(range, scope)| { - let highlighter = - highlighting::Highlighter::new(&self.theme); - let _ = self.stack.apply(&scope); + let _ = stack.apply(&scope); if range.is_empty() { None @@ -168,8 +186,7 @@ mod highlighter { Some(( range, Highlight( - highlighter - .style_mod_for_stack(&self.stack.scopes), + highlighter.style_mod_for_stack(&stack.scopes), ), )) } -- cgit From 86d396cf8bede8155bdd4a7d3f115a0108c67297 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 23:15:38 +0200 Subject: Avoid adding unnecessary spans when syntax highlighting --- Cargo.toml | 2 +- graphics/src/text/editor.rs | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 70f84460..ac59085d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,7 +154,7 @@ winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8 [patch.crates-io.cosmic-text] git = "https://github.com/hecrj/cosmic-text.git" -rev = "cb83458e7d0b84ef37c5beb72dda5046d7d343a6" +branch = "editor-fixes" [patch.crates-io.rangemap] git = "https://github.com/hecrj/rangemap.git" diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 47c210bd..95061c3c 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -569,17 +569,19 @@ impl editor::Editor for Editor { for (range, highlight) in highlighter.highlight_line(line.text()) { let format = format_highlight(&highlight); - list.add_span( - range, - cosmic_text::Attrs { - color_opt: format.color.map(text::to_color), - ..if let Some(font) = format.font { - text::to_attributes(font) - } else { - attributes - } - }, - ); + if format.color.is_some() || format.font.is_some() { + list.add_span( + range, + cosmic_text::Attrs { + color_opt: format.color.map(text::to_color), + ..if let Some(font) = format.font { + text::to_attributes(font) + } else { + attributes + } + }, + ); + } } let _ = line.set_attrs_list(list); -- cgit From 8f8528a4ccee049aba779fe86cda786a52afac30 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 23:20:15 +0200 Subject: Fix unnecessary dereference in `editor` example --- examples/editor/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 1235d38b..74649676 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -113,7 +113,7 @@ mod highlighter { .find_syntax_by_token(&settings.token) .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); - let parser = parsing::ParseState::new(&syntax); + let parser = parsing::ParseState::new(syntax); let stack = parsing::ScopeStack::new(); let theme = highlighting::ThemeSet::load_defaults() -- cgit From d1440ceca6340d045e556eb05354c254881732f0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 13:39:47 +0200 Subject: Find correct `last_visible_line` in `Editor::highlight` --- graphics/src/text/editor.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 95061c3c..18c9b572 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -538,11 +538,27 @@ impl editor::Editor for Editor { let internal = self.internal(); let buffer = internal.editor.buffer(); - let scroll = buffer.scroll(); - let visible_lines = buffer.visible_lines(); - let last_visible_line = ((scroll + visible_lines) as usize) - .min(buffer.lines.len()) - .saturating_sub(1); + let mut window = buffer.scroll() + buffer.visible_lines(); + + let last_visible_line = buffer + .lines + .iter() + .enumerate() + .find_map(|(i, line)| { + let visible_lines = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() as i32; + + if window > visible_lines { + window -= visible_lines; + None + } else { + Some(i) + } + }) + .unwrap_or(buffer.lines.len()); let current_line = highlighter.current_line(); -- cgit From a01b123cec1b57a9100d56f567fcfbf91967b12f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 13:57:47 +0200 Subject: Shape as needed only in `update` during `layout` --- graphics/src/text/editor.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 18c9b572..59096e74 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -442,8 +442,6 @@ impl editor::Editor for Editor { } } - editor.shape_as_needed(font_system.raw()); - self.0 = Some(Arc::new(internal)); } @@ -487,8 +485,6 @@ impl editor::Editor for Editor { internal.font = new_font; internal.topmost_line_changed = Some(0); - - internal.editor.shape_as_needed(font_system.raw()); } let metrics = internal.editor.buffer().metrics(); @@ -526,6 +522,8 @@ impl editor::Editor for Editor { new_highlighter.change_line(topmost_line_changed); } + internal.editor.shape_as_needed(font_system.raw()); + self.0 = Some(Arc::new(internal)); } -- cgit From b5466f41ca33452fb0d4e8470856c027d3b26e39 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 13:58:39 +0200 Subject: Fix inconsistent `expect` messages in `text::editor` --- graphics/src/text/editor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 59096e74..de1b998b 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -43,7 +43,7 @@ impl Editor { fn internal(&self) -> &Arc { self.0 .as_ref() - .expect("editor should always be initialized") + .expect("Editor should always be initialized") } } @@ -458,7 +458,7 @@ impl editor::Editor for Editor { new_highlighter: &mut impl Highlighter, ) { let editor = - self.0.take().expect("editor should always be initialized"); + self.0.take().expect("Editor should always be initialized"); let mut internal = Arc::try_unwrap(editor) .expect("Editor cannot have multiple strong references"); @@ -565,7 +565,7 @@ impl editor::Editor for Editor { } let editor = - self.0.take().expect("editor should always be initialized"); + self.0.take().expect("Editor should always be initialized"); let mut internal = Arc::try_unwrap(editor) .expect("Editor cannot have multiple strong references"); -- cgit From 61ef8f3249218b301d434d04c483ba70562c1df4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 13:58:55 +0200 Subject: Update `version` properly when `FontSystem` changes in `text::editor` --- graphics/src/text/editor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index de1b998b..4673fce3 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -472,6 +472,9 @@ impl editor::Editor for Editor { for line in internal.editor.buffer_mut().lines.iter_mut() { line.reset(); } + + internal.version = font_system.version(); + internal.topmost_line_changed = Some(0); } if new_font != internal.font { -- cgit From 8446fe6de52fa68077d23d39f728f79a29b52f00 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 14:38:54 +0200 Subject: Implement theme selector in `editor` example --- core/src/text/highlighter.rs | 7 ++- examples/editor/src/main.rs | 101 +++++++++++++++++++++++++++++++++++-------- widget/src/text_editor.rs | 13 +++++- 3 files changed, 99 insertions(+), 22 deletions(-) diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs index a929826f..b462d083 100644 --- a/core/src/text/highlighter.rs +++ b/core/src/text/highlighter.rs @@ -1,10 +1,9 @@ use crate::Color; -use std::hash::Hash; use std::ops::Range; pub trait Highlighter: 'static { - type Settings: Hash; + type Settings: PartialEq + Clone; type Highlight; type Iterator<'a>: Iterator, Self::Highlight)> @@ -13,6 +12,8 @@ pub trait Highlighter: 'static { fn new(settings: &Self::Settings) -> Self; + fn update(&mut self, new_settings: &Self::Settings); + fn change_line(&mut self, line: usize); fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>; @@ -38,6 +39,8 @@ impl Highlighter for PlainText { Self } + fn update(&mut self, _new_settings: &Self::Settings) {} + fn change_line(&mut self, _line: usize) {} fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> { diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 74649676..fa35ba0f 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,5 +1,5 @@ -use iced::widget::{container, text_editor}; -use iced::{Element, Font, Sandbox, Settings, Theme}; +use iced::widget::{column, horizontal_space, pick_list, row, text_editor}; +use iced::{Element, Font, Length, Sandbox, Settings, Theme}; use highlighter::Highlighter; @@ -9,11 +9,13 @@ pub fn main() -> iced::Result { struct Editor { content: text_editor::Content, + theme: highlighter::Theme, } #[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), + ThemeSelected(highlighter::Theme), } impl Sandbox for Editor { @@ -21,9 +23,8 @@ impl Sandbox for Editor { fn new() -> Self { Self { - content: text_editor::Content::with(include_str!( - "../../../README.md" - )), + content: text_editor::Content::with(include_str!("main.rs")), + theme: highlighter::Theme::SolarizedDark, } } @@ -36,18 +37,33 @@ impl Sandbox for Editor { Message::Edit(action) => { self.content.edit(action); } + Message::ThemeSelected(theme) => { + self.theme = theme; + } } } fn view(&self) -> Element { - container( + column![ + row![ + horizontal_space(Length::Fill), + pick_list( + highlighter::Theme::ALL, + Some(self.theme), + Message::ThemeSelected + ) + .padding([5, 10]) + ] + .spacing(10), text_editor(&self.content) .on_edit(Message::Edit) .font(Font::with_name("Hasklug Nerd Font Mono")) .highlight::(highlighter::Settings { - token: String::from("md"), + theme: self.theme, + extension: String::from("rs"), }), - ) + ] + .spacing(10) .padding(20) .into() } @@ -60,21 +76,52 @@ impl Sandbox for Editor { mod highlighter { use iced::advanced::text::highlighter; use iced::widget::text_editor; - use iced::{Color, Font, Theme}; + use iced::{Color, Font}; use std::ops::Range; use syntect::highlighting; use syntect::parsing::{self, SyntaxReference}; - #[derive(Debug, Clone, Hash)] + #[derive(Debug, Clone, PartialEq)] pub struct Settings { - pub token: String, + pub theme: Theme, + pub extension: String, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Theme { + SolarizedDark, + InspiredGitHub, + Base16Mocha, + } + + impl Theme { + pub const ALL: &[Self] = + &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha]; + + fn key(&self) -> &'static str { + match self { + Theme::InspiredGitHub => "InspiredGitHub", + Theme::Base16Mocha => "base16-mocha.dark", + Theme::SolarizedDark => "Solarized (dark)", + } + } + } + + impl std::fmt::Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Theme::InspiredGitHub => write!(f, "Inspired GitHub"), + Theme::Base16Mocha => write!(f, "Mocha"), + Theme::SolarizedDark => write!(f, "Solarized Dark"), + } + } } pub struct Highlight(highlighting::StyleModifier); impl text_editor::Highlight for Highlight { - fn format(&self, _theme: &Theme) -> highlighter::Format { + fn format(&self, _theme: &iced::Theme) -> highlighter::Format { highlighter::Format { color: self.0.foreground.map(|color| { Color::from_rgba8( @@ -92,8 +139,8 @@ mod highlighter { pub struct Highlighter { syntaxes: parsing::SyntaxSet, syntax: SyntaxReference, - caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, theme: highlighting::Theme, + caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, current_line: usize, } @@ -110,26 +157,42 @@ mod highlighter { let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); let syntax = syntaxes - .find_syntax_by_token(&settings.token) + .find_syntax_by_token(&settings.extension) .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); - let parser = parsing::ParseState::new(syntax); - let stack = parsing::ScopeStack::new(); - let theme = highlighting::ThemeSet::load_defaults() .themes - .remove("base16-mocha.dark") + .remove(settings.theme.key()) .unwrap(); + let parser = parsing::ParseState::new(syntax); + let stack = parsing::ScopeStack::new(); + Highlighter { syntax: syntax.clone(), syntaxes, - caches: vec![(parser, stack)], theme, + caches: vec![(parser, stack)], current_line: 0, } } + fn update(&mut self, new_settings: &Self::Settings) { + self.syntax = self + .syntaxes + .find_syntax_by_token(&new_settings.extension) + .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()) + .clone(); + + self.theme = highlighting::ThemeSet::load_defaults() + .themes + .remove(new_settings.theme.key()) + .unwrap(); + + // Restart the highlighter + self.change_line(0); + } + fn change_line(&mut self, line: usize) { let snapshot = line / LINES_PER_SNAPSHOT; diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index c30e185f..0cde2c98 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -193,11 +193,12 @@ where } } -struct State { +struct State { is_focused: bool, last_click: Option, drag_click: Option, highlighter: RefCell, + highlighter_settings: Highlighter::Settings, } impl<'a, Highlighter, Message, Renderer> Widget @@ -220,6 +221,7 @@ where highlighter: RefCell::new(Highlighter::new( &self.highlighter_settings, )), + highlighter_settings: self.highlighter_settings.clone(), }) } @@ -240,6 +242,15 @@ where let mut internal = self.content.0.borrow_mut(); let state = tree.state.downcast_mut::>(); + if state.highlighter_settings != self.highlighter_settings { + state + .highlighter + .borrow_mut() + .update(&self.highlighter_settings); + + state.highlighter_settings = self.highlighter_settings.clone(); + } + internal.editor.update( limits.pad(self.padding).max(), self.font.unwrap_or_else(|| renderer.default_font()), -- cgit From e7326f0af6f16cf2ff04fbac93bf296a044923f4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 19:07:41 +0200 Subject: Flesh out the `editor` example a bit more --- core/src/renderer/null.rs | 4 + core/src/text/editor.rs | 8 ++ examples/editor/Cargo.toml | 8 +- examples/editor/fonts/icons.ttf | Bin 0 -> 6352 bytes examples/editor/src/main.rs | 287 ++++++++++++++++++++++++++++++++++++---- graphics/src/text/editor.rs | 8 +- src/settings.rs | 8 ++ widget/src/text_editor.rs | 4 + winit/src/application.rs | 9 +- winit/src/settings.rs | 4 + 10 files changed, 312 insertions(+), 28 deletions(-) create mode 100644 examples/editor/fonts/icons.ttf diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 21597c8e..da0f32de 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -125,6 +125,10 @@ impl text::Editor for () { text::editor::Cursor::Caret(Point::ORIGIN) } + fn cursor_position(&self) -> (usize, usize) { + (0, 0) + } + fn selection(&self) -> Option { None } diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 2144715f..13bafc3d 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -12,6 +12,8 @@ pub trait Editor: Sized + Default { fn cursor(&self) -> Cursor; + fn cursor_position(&self) -> (usize, usize); + fn selection(&self) -> Option; fn line(&self, index: usize) -> Option<&str>; @@ -52,6 +54,12 @@ pub enum Action { Drag(Point), } +impl Action { + pub fn is_edit(&self) -> bool { + matches!(self, Self::Edit(_)) + } +} + #[derive(Debug, Clone, PartialEq)] pub enum Edit { Insert(char), diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index 930ee592..eeb34aa1 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,6 +7,10 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["advanced", "debug"] +iced.features = ["advanced", "tokio", "debug"] -syntect = "5.1" \ No newline at end of file +tokio.workspace = true +tokio.features = ["fs"] + +syntect = "5.1" +rfd = "0.12" diff --git a/examples/editor/fonts/icons.ttf b/examples/editor/fonts/icons.ttf new file mode 100644 index 00000000..393c6922 Binary files /dev/null and b/examples/editor/fonts/icons.ttf differ diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index fa35ba0f..09c4b9b5 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,70 +1,218 @@ -use iced::widget::{column, horizontal_space, pick_list, row, text_editor}; -use iced::{Element, Font, Length, Sandbox, Settings, Theme}; +use iced::executor; +use iced::theme::{self, Theme}; +use iced::widget::{ + button, column, container, horizontal_space, pick_list, row, text, + text_editor, tooltip, +}; +use iced::{Application, Command, Element, Font, Length, Settings}; use highlighter::Highlighter; +use std::ffi; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + pub fn main() -> iced::Result { - Editor::run(Settings::default()) + Editor::run(Settings { + fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], + default_font: Font { + monospaced: true, + ..Font::with_name("Hasklug Nerd Font Mono") + }, + ..Settings::default() + }) } struct Editor { + file: Option, content: text_editor::Content, theme: highlighter::Theme, + is_loading: bool, + is_dirty: bool, } #[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), ThemeSelected(highlighter::Theme), + NewFile, + OpenFile, + FileOpened(Result<(PathBuf, Arc), Error>), + SaveFile, + FileSaved(Result), } -impl Sandbox for Editor { +impl Application for Editor { type Message = Message; - - fn new() -> Self { - Self { - content: text_editor::Content::with(include_str!("main.rs")), - theme: highlighter::Theme::SolarizedDark, - } + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command) { + ( + Self { + file: None, + content: text_editor::Content::new(), + theme: highlighter::Theme::SolarizedDark, + is_loading: true, + is_dirty: false, + }, + Command::perform(load_file(default_file()), Message::FileOpened), + ) } fn title(&self) -> String { String::from("Editor - Iced") } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command { match message { Message::Edit(action) => { + self.is_dirty = self.is_dirty || action.is_edit(); + self.content.edit(action); + + Command::none() } Message::ThemeSelected(theme) => { self.theme = theme; + + Command::none() + } + Message::NewFile => { + if !self.is_loading { + self.file = None; + self.content = text_editor::Content::new(); + } + + Command::none() + } + Message::OpenFile => { + if self.is_loading { + Command::none() + } else { + self.is_loading = true; + + Command::perform(open_file(), Message::FileOpened) + } + } + Message::FileOpened(result) => { + self.is_loading = false; + self.is_dirty = false; + + if let Ok((path, contents)) = result { + self.file = Some(path); + self.content = text_editor::Content::with(&contents); + } + + Command::none() + } + Message::SaveFile => { + if self.is_loading { + Command::none() + } else { + self.is_loading = true; + + let mut contents = self.content.lines().enumerate().fold( + String::new(), + |mut contents, (i, line)| { + if i > 0 { + contents.push_str("\n"); + } + + contents.push_str(&line); + + contents + }, + ); + + if !contents.ends_with("\n") { + contents.push_str("\n"); + } + + Command::perform( + save_file(self.file.clone(), contents), + Message::FileSaved, + ) + } + } + Message::FileSaved(result) => { + self.is_loading = false; + + if let Ok(path) = result { + self.file = Some(path); + self.is_dirty = false; + } + + Command::none() } } } fn view(&self) -> Element { + let controls = row![ + action(new_icon(), "New file", Some(Message::NewFile)), + action( + open_icon(), + "Open file", + (!self.is_loading).then_some(Message::OpenFile) + ), + action( + save_icon(), + "Save file", + self.is_dirty.then_some(Message::SaveFile) + ), + horizontal_space(Length::Fill), + pick_list( + highlighter::Theme::ALL, + Some(self.theme), + Message::ThemeSelected + ) + .text_size(14) + .padding([5, 10]) + ] + .spacing(10); + + let status = row![ + text(if let Some(path) = &self.file { + let path = path.display().to_string(); + + if path.len() > 60 { + format!("...{}", &path[path.len() - 40..]) + } else { + path + } + } else { + String::from("New file") + }), + horizontal_space(Length::Fill), + text({ + let (line, column) = self.content.cursor_position(); + + format!("{}:{}", line + 1, column + 1) + }) + ] + .spacing(10); + column![ - row![ - horizontal_space(Length::Fill), - pick_list( - highlighter::Theme::ALL, - Some(self.theme), - Message::ThemeSelected - ) - .padding([5, 10]) - ] - .spacing(10), + controls, text_editor(&self.content) .on_edit(Message::Edit) - .font(Font::with_name("Hasklug Nerd Font Mono")) .highlight::(highlighter::Settings { theme: self.theme, - extension: String::from("rs"), + extension: self + .file + .as_deref() + .and_then(Path::extension) + .and_then(ffi::OsStr::to_str) + .map(str::to_string) + .unwrap_or(String::from("rs")), }), + status, ] .spacing(10) - .padding(20) + .padding(10) .into() } @@ -73,6 +221,97 @@ impl Sandbox for Editor { } } +#[derive(Debug, Clone)] +pub enum Error { + DialogClosed, + IoError(io::ErrorKind), +} + +fn default_file() -> PathBuf { + PathBuf::from(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))) +} + +async fn open_file() -> Result<(PathBuf, Arc), Error> { + let picked_file = rfd::AsyncFileDialog::new() + .set_title("Open a text file...") + .pick_file() + .await + .ok_or(Error::DialogClosed)?; + + load_file(picked_file.path().to_owned()).await +} + +async fn load_file(path: PathBuf) -> Result<(PathBuf, Arc), Error> { + let contents = tokio::fs::read_to_string(&path) + .await + .map(Arc::new) + .map_err(|error| Error::IoError(error.kind()))?; + + Ok((path, contents)) +} + +async fn save_file( + path: Option, + contents: String, +) -> Result { + let path = if let Some(path) = path { + path + } else { + rfd::AsyncFileDialog::new() + .save_file() + .await + .as_ref() + .map(rfd::FileHandle::path) + .map(Path::to_owned) + .ok_or(Error::DialogClosed)? + }; + + let _ = tokio::fs::write(&path, contents) + .await + .map_err(|error| Error::IoError(error.kind()))?; + + Ok(path) +} + +fn action<'a, Message: Clone + 'a>( + content: impl Into>, + label: &'a str, + on_press: Option, +) -> Element<'a, Message> { + let action = + button(container(content).width(Length::Fill).center_x()).width(40); + + if let Some(on_press) = on_press { + tooltip( + action.on_press(on_press), + label, + tooltip::Position::FollowCursor, + ) + .style(theme::Container::Box) + .into() + } else { + action.style(theme::Button::Secondary).into() + } +} + +fn new_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e800}') +} + +fn save_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e801}') +} + +fn open_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0f115}') +} + +fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> { + const ICON_FONT: Font = Font::with_name("editor-icons"); + + text(codepoint).font(ICON_FONT).into() +} + mod highlighter { use iced::advanced::text::highlighter; use iced::widget::text_editor; diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 4673fce3..dfb91f34 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -221,6 +221,12 @@ impl editor::Editor for Editor { } } + fn cursor_position(&self) -> (usize, usize) { + let cursor = self.internal().editor.cursor(); + + (cursor.line, cursor.index) + } + fn perform(&mut self, action: Action) { let mut font_system = text::font_system().write().expect("Write font system"); @@ -559,7 +565,7 @@ impl editor::Editor for Editor { Some(i) } }) - .unwrap_or(buffer.lines.len()); + .unwrap_or(buffer.lines.len().saturating_sub(1)); let current_line = highlighter.current_line(); diff --git a/src/settings.rs b/src/settings.rs index d9778d7e..6b9ce095 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,6 +2,8 @@ use crate::window; use crate::{Font, Pixels}; +use std::borrow::Cow; + /// The settings of an application. #[derive(Debug, Clone)] pub struct Settings { @@ -21,6 +23,9 @@ pub struct Settings { /// [`Application`]: crate::Application pub flags: Flags, + /// The fonts to load on boot. + pub fonts: Vec>, + /// The default [`Font`] to be used. /// /// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif). @@ -62,6 +67,7 @@ impl Settings { flags, id: default_settings.id, window: default_settings.window, + fonts: default_settings.fonts, default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, antialiasing: default_settings.antialiasing, @@ -79,6 +85,7 @@ where id: None, window: Default::default(), flags: Default::default(), + fonts: Default::default(), default_font: Default::default(), default_text_size: Pixels(16.0), antialiasing: false, @@ -93,6 +100,7 @@ impl From> for iced_winit::Settings { id: settings.id, window: settings.window.into(), flags: settings.flags, + fonts: settings.fonts, exit_on_close_request: settings.exit_on_close_request, } } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 0cde2c98..970ec031 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -182,6 +182,10 @@ where pub fn selection(&self) -> Option { self.0.borrow().editor.selection() } + + pub fn cursor_position(&self) -> (usize, usize) { + self.0.borrow().editor.cursor_position() + } } impl Default for Content diff --git a/winit/src/application.rs b/winit/src/application.rs index d1689452..e80e9783 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -193,7 +193,14 @@ where }; } - let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; + let (compositor, mut renderer) = + C::new(compositor_settings, Some(&window))?; + + for font in settings.fonts { + use crate::core::text::Renderer; + + renderer.load_font(font); + } let (mut event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, mut control_receiver) = mpsc::unbounded(); diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 8d3e1b47..b4a1dd61 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -33,6 +33,7 @@ use crate::Position; use winit::monitor::MonitorHandle; use winit::window::WindowBuilder; +use std::borrow::Cow; use std::fmt; /// The settings of an application. @@ -52,6 +53,9 @@ pub struct Settings { /// [`Application`]: crate::Application pub flags: Flags, + /// The fonts to load on boot. + pub fonts: Vec>, + /// Whether the [`Application`] should exit when the user requests the /// window to close (e.g. the user presses the close button). /// -- cgit From 161a971d065b3254a2f11cb374d2c94c2d67646b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 19:08:57 +0200 Subject: Fix `clippy` lints --- examples/editor/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 09c4b9b5..785dfb3b 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -118,7 +118,7 @@ impl Application for Editor { String::new(), |mut contents, (i, line)| { if i > 0 { - contents.push_str("\n"); + contents.push('\n'); } contents.push_str(&line); @@ -127,8 +127,8 @@ impl Application for Editor { }, ); - if !contents.ends_with("\n") { - contents.push_str("\n"); + if !contents.ends_with('\n') { + contents.push('\n'); } Command::perform( @@ -266,7 +266,7 @@ async fn save_file( .ok_or(Error::DialogClosed)? }; - let _ = tokio::fs::write(&path, contents) + tokio::fs::write(&path, contents) .await .map_err(|error| Error::IoError(error.kind()))?; -- cgit From 8eec0033dee816bfcc102fc4f511c8bfe08c14ee Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 19:24:09 +0200 Subject: Remove unnecessary `monospaced` flag in `Font` --- core/src/font.rs | 4 ---- examples/editor/src/main.rs | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/core/src/font.rs b/core/src/font.rs index 7f647847..2b68decf 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -12,8 +12,6 @@ pub struct Font { pub stretch: Stretch, /// The [`Style`] of the [`Font`]. pub style: Style, - /// Whether if the [`Font`] is monospaced or not. - pub monospaced: bool, } impl Font { @@ -23,13 +21,11 @@ impl Font { weight: Weight::Normal, stretch: Stretch::Normal, style: Style::Normal, - monospaced: false, }; /// A monospaced font with normal [`Weight`]. pub const MONOSPACE: Font = Font { family: Family::Monospace, - monospaced: true, ..Self::DEFAULT }; diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 785dfb3b..5018b3cb 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -16,10 +16,7 @@ use std::sync::Arc; pub fn main() -> iced::Result { Editor::run(Settings { fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], - default_font: Font { - monospaced: true, - ..Font::with_name("Hasklug Nerd Font Mono") - }, + default_font: Font::with_name("Hasklug Nerd Font Mono"), ..Settings::default() }) } -- cgit From d1d0b3aaee84003278b9db3e86687e776f20b346 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 20:14:38 +0200 Subject: Use `Font::MONOSPACE` in `editor` example --- Cargo.toml | 2 +- examples/editor/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac59085d..e887afc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,7 +154,7 @@ winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8 [patch.crates-io.cosmic-text] git = "https://github.com/hecrj/cosmic-text.git" -branch = "editor-fixes" +branch = "respect-fontconfig-aliases" [patch.crates-io.rangemap] git = "https://github.com/hecrj/rangemap.git" diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 5018b3cb..277eb3e9 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -16,7 +16,7 @@ use std::sync::Arc; pub fn main() -> iced::Result { Editor::run(Settings { fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], - default_font: Font::with_name("Hasklug Nerd Font Mono"), + default_font: Font::MONOSPACE, ..Settings::default() }) } -- cgit From 36e867de693d4e9fc64da3d9d7745a5b1398d8a5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 20:59:39 +0200 Subject: Fix `lint` and `test` GitHub CI workflows --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6fd98374..af34bb13 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,7 +2,7 @@ name: Lint on: [push, pull_request] jobs: all: - runs-on: ubuntu-latest + runs-on: macOS-latest steps: - uses: hecrj/setup-rust-action@v1 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac8d27f9..215b616b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: run: | export DEBIAN_FRONTED=noninteractive sudo apt-get -qq update - sudo apt-get install -y libxkbcommon-dev + sudo apt-get install -y libxkbcommon-dev libgtk-3-dev - name: Run tests run: | cargo test --verbose --workspace -- cgit From 4e757a26d0c1c58001f31cf0592131cd5ad886ad Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 01:18:06 +0200 Subject: Implement `Scroll` action in `text::editor` --- core/src/text/editor.rs | 1 + graphics/src/text/editor.rs | 6 ++++++ widget/src/text_editor.rs | 12 ++++++++++++ 3 files changed, 19 insertions(+) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 13bafc3d..e9d66ce9 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -52,6 +52,7 @@ pub enum Action { Edit(Edit), Click(Point), Drag(Point), + Scroll { lines: i32 }, } impl Action { diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index dfb91f34..a05312dc 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -446,6 +446,12 @@ impl editor::Editor for Editor { } } } + Action::Scroll { lines } => { + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { lines }, + ); + } } self.0 = Some(Arc::new(internal)); diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 970ec031..ad12a076 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -521,6 +521,18 @@ impl Update { } _ => None, }, + mouse::Event::WheelScrolled { delta } => { + action(Action::Scroll { + lines: match delta { + mouse::ScrollDelta::Lines { y, .. } => { + -y as i32 * 4 + } + mouse::ScrollDelta::Pixels { y, .. } => { + -y.signum() as i32 + } + }, + }) + } _ => None, }, Event::Keyboard(event) => match event { -- cgit From 06dc12bfbf75958c6534306b3d1b57ae47bdb37a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 19:35:28 +0200 Subject: Simplify `editor` example --- examples/editor/src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 277eb3e9..6def2082 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{Application, Command, Element, Font, Length, Settings}; +use iced::{Alignment, Application, Command, Element, Font, Length, Settings}; use highlighter::Highlighter; @@ -169,7 +169,8 @@ impl Application for Editor { .text_size(14) .padding([5, 10]) ] - .spacing(10); + .spacing(10) + .align_items(Alignment::Center); let status = row![ text(if let Some(path) = &self.file { @@ -275,8 +276,7 @@ fn action<'a, Message: Clone + 'a>( label: &'a str, on_press: Option, ) -> Element<'a, Message> { - let action = - button(container(content).width(Length::Fill).center_x()).width(40); + let action = button(container(content).width(30).center_x()); if let Some(on_press) = on_press { tooltip( @@ -316,7 +316,7 @@ mod highlighter { use std::ops::Range; use syntect::highlighting; - use syntect::parsing::{self, SyntaxReference}; + use syntect::parsing; #[derive(Debug, Clone, PartialEq)] pub struct Settings { @@ -374,7 +374,7 @@ mod highlighter { pub struct Highlighter { syntaxes: parsing::SyntaxSet, - syntax: SyntaxReference, + syntax: parsing::SyntaxReference, theme: highlighting::Theme, caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, current_line: usize, -- cgit From c0a141ab026f5686d6bd92c8807b174396cb9105 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 19:39:23 +0200 Subject: Save file on `Cmd+S` in `editor` example --- examples/editor/src/main.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 6def2082..36d4287c 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,10 +1,14 @@ use iced::executor; +use iced::keyboard; use iced::theme::{self, Theme}; use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{Alignment, Application, Command, Element, Font, Length, Settings}; +use iced::{ + Alignment, Application, Command, Element, Font, Length, Settings, + Subscription, +}; use highlighter::Highlighter; @@ -147,6 +151,15 @@ impl Application for Editor { } } + fn subscription(&self) -> Subscription { + keyboard::on_key_press(|key_code, modifiers| match key_code { + keyboard::KeyCode::S if modifiers.command() => { + Some(Message::SaveFile) + } + _ => None, + }) + } + fn view(&self) -> Element { let controls = row![ action(new_icon(), "New file", Some(Message::NewFile)), -- cgit From f806d001e6fb44b5a45029ca257261e6e0d4d4b2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 20:48:50 +0200 Subject: Introduce new `iced_highlighter` subcrate --- Cargo.toml | 10 +- core/src/text/highlighter.rs | 11 +- examples/editor/Cargo.toml | 2 +- examples/editor/src/main.rs | 251 +++---------------------------------------- highlighter/Cargo.toml | 16 +++ highlighter/src/lib.rs | 225 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + style/src/text_editor.rs | 16 +-- widget/src/text_editor.rs | 28 ++++- 9 files changed, 302 insertions(+), 260 deletions(-) create mode 100644 highlighter/Cargo.toml create mode 100644 highlighter/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index e887afc0..8899fa67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ system = ["iced_winit/system"] web-colors = ["iced_renderer/web-colors"] # Enables the WebGL backend, replacing WebGPU webgl = ["iced_renderer/webgl"] +# Enables the syntax `highlighter` module +highlighter = ["iced_highlighter"] # Enables the advanced module advanced = [] @@ -58,6 +60,9 @@ iced_widget.workspace = true iced_winit.features = ["application"] iced_winit.workspace = true +iced_highlighter.workspace = true +iced_highlighter.optional = true + thiserror.workspace = true image.workspace = true @@ -78,8 +83,9 @@ members = [ "core", "futures", "graphics", - "runtime", + "highlighter", "renderer", + "runtime", "style", "tiny_skia", "wgpu", @@ -103,6 +109,7 @@ iced = { version = "0.12", path = "." } iced_core = { version = "0.12", path = "core" } iced_futures = { version = "0.12", path = "futures" } iced_graphics = { version = "0.12", path = "graphics" } +iced_highlighter = { version = "0.12", path = "highlighter" } iced_renderer = { version = "0.12", path = "renderer" } iced_runtime = { version = "0.12", path = "runtime" } iced_style = { version = "0.12", path = "style" } @@ -137,6 +144,7 @@ resvg = "0.35" rustc-hash = "1.0" smol = "1.0" softbuffer = "0.2" +syntect = "5.1" sysinfo = "0.28" thiserror = "1.0" tiny-skia = "0.10" diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs index b462d083..9a9cff89 100644 --- a/core/src/text/highlighter.rs +++ b/core/src/text/highlighter.rs @@ -52,8 +52,17 @@ impl Highlighter for PlainText { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Format { pub color: Option, pub font: Option, } + +impl Default for Format { + fn default() -> Self { + Self { + color: None, + font: None, + } + } +} diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index eeb34aa1..a77b1e9f 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["advanced", "tokio", "debug"] +iced.features = ["highlighter", "tokio", "debug"] tokio.workspace = true tokio.features = ["fs"] diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 36d4287c..d513090f 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,4 +1,5 @@ use iced::executor; +use iced::highlighter::{self, Highlighter}; use iced::keyboard; use iced::theme::{self, Theme}; use iced::widget::{ @@ -10,8 +11,6 @@ use iced::{ Subscription, }; -use highlighter::Highlighter; - use std::ffi; use std::io; use std::path::{Path, PathBuf}; @@ -210,16 +209,19 @@ impl Application for Editor { controls, text_editor(&self.content) .on_edit(Message::Edit) - .highlight::(highlighter::Settings { - theme: self.theme, - extension: self - .file - .as_deref() - .and_then(Path::extension) - .and_then(ffi::OsStr::to_str) - .map(str::to_string) - .unwrap_or(String::from("rs")), - }), + .highlight::( + highlighter::Settings { + theme: self.theme, + extension: self + .file + .as_deref() + .and_then(Path::extension) + .and_then(ffi::OsStr::to_str) + .map(str::to_string) + .unwrap_or(String::from("rs")), + }, + |highlight, _theme| highlight.to_format() + ), status, ] .spacing(10) @@ -321,228 +323,3 @@ fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> { text(codepoint).font(ICON_FONT).into() } - -mod highlighter { - use iced::advanced::text::highlighter; - use iced::widget::text_editor; - use iced::{Color, Font}; - - use std::ops::Range; - use syntect::highlighting; - use syntect::parsing; - - #[derive(Debug, Clone, PartialEq)] - pub struct Settings { - pub theme: Theme, - pub extension: String, - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum Theme { - SolarizedDark, - InspiredGitHub, - Base16Mocha, - } - - impl Theme { - pub const ALL: &[Self] = - &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha]; - - fn key(&self) -> &'static str { - match self { - Theme::InspiredGitHub => "InspiredGitHub", - Theme::Base16Mocha => "base16-mocha.dark", - Theme::SolarizedDark => "Solarized (dark)", - } - } - } - - impl std::fmt::Display for Theme { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Theme::InspiredGitHub => write!(f, "Inspired GitHub"), - Theme::Base16Mocha => write!(f, "Mocha"), - Theme::SolarizedDark => write!(f, "Solarized Dark"), - } - } - } - - pub struct Highlight(highlighting::StyleModifier); - - impl text_editor::Highlight for Highlight { - fn format(&self, _theme: &iced::Theme) -> highlighter::Format { - highlighter::Format { - color: self.0.foreground.map(|color| { - Color::from_rgba8( - color.r, - color.g, - color.b, - color.a as f32 / 255.0, - ) - }), - font: None, - } - } - } - - pub struct Highlighter { - syntaxes: parsing::SyntaxSet, - syntax: parsing::SyntaxReference, - theme: highlighting::Theme, - caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, - current_line: usize, - } - - const LINES_PER_SNAPSHOT: usize = 50; - - impl highlighter::Highlighter for Highlighter { - type Settings = Settings; - type Highlight = Highlight; - - type Iterator<'a> = - Box, Self::Highlight)> + 'a>; - - fn new(settings: &Self::Settings) -> Self { - let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); - - let syntax = syntaxes - .find_syntax_by_token(&settings.extension) - .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); - - let theme = highlighting::ThemeSet::load_defaults() - .themes - .remove(settings.theme.key()) - .unwrap(); - - let parser = parsing::ParseState::new(syntax); - let stack = parsing::ScopeStack::new(); - - Highlighter { - syntax: syntax.clone(), - syntaxes, - theme, - caches: vec![(parser, stack)], - current_line: 0, - } - } - - fn update(&mut self, new_settings: &Self::Settings) { - self.syntax = self - .syntaxes - .find_syntax_by_token(&new_settings.extension) - .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()) - .clone(); - - self.theme = highlighting::ThemeSet::load_defaults() - .themes - .remove(new_settings.theme.key()) - .unwrap(); - - // Restart the highlighter - self.change_line(0); - } - - fn change_line(&mut self, line: usize) { - let snapshot = line / LINES_PER_SNAPSHOT; - - if snapshot <= self.caches.len() { - self.caches.truncate(snapshot); - self.current_line = snapshot * LINES_PER_SNAPSHOT; - } else { - self.caches.truncate(1); - self.current_line = 0; - } - - let (parser, stack) = - self.caches.last().cloned().unwrap_or_else(|| { - ( - parsing::ParseState::new(&self.syntax), - parsing::ScopeStack::new(), - ) - }); - - self.caches.push((parser, stack)); - } - - fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { - if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() { - let (parser, stack) = - self.caches.last().expect("Caches must not be empty"); - - self.caches.push((parser.clone(), stack.clone())); - } - - self.current_line += 1; - - let (parser, stack) = - self.caches.last_mut().expect("Caches must not be empty"); - - let ops = - parser.parse_line(line, &self.syntaxes).unwrap_or_default(); - - let highlighter = highlighting::Highlighter::new(&self.theme); - - Box::new( - ScopeRangeIterator { - ops, - line_length: line.len(), - index: 0, - last_str_index: 0, - } - .filter_map(move |(range, scope)| { - let _ = stack.apply(&scope); - - if range.is_empty() { - None - } else { - Some(( - range, - Highlight( - highlighter.style_mod_for_stack(&stack.scopes), - ), - )) - } - }), - ) - } - - fn current_line(&self) -> usize { - self.current_line - } - } - - pub struct ScopeRangeIterator { - ops: Vec<(usize, parsing::ScopeStackOp)>, - line_length: usize, - index: usize, - last_str_index: usize, - } - - impl Iterator for ScopeRangeIterator { - type Item = (std::ops::Range, parsing::ScopeStackOp); - - fn next(&mut self) -> Option { - if self.index > self.ops.len() { - return None; - } - - let next_str_i = if self.index == self.ops.len() { - self.line_length - } else { - self.ops[self.index].0 - }; - - let range = self.last_str_index..next_str_i; - self.last_str_index = next_str_i; - - let op = if self.index == 0 { - parsing::ScopeStackOp::Noop - } else { - self.ops[self.index - 1].1.clone() - }; - - self.index += 1; - Some((range, op)) - } - } -} diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml new file mode 100644 index 00000000..311d2998 --- /dev/null +++ b/highlighter/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "iced_highlighter" +description = "A syntax higlighter for iced" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true + +[dependencies] +iced_core.workspace = true + +syntect.workspace = true diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs new file mode 100644 index 00000000..79cfafcd --- /dev/null +++ b/highlighter/src/lib.rs @@ -0,0 +1,225 @@ +use iced_core as core; + +use crate::core::text::highlighter::{self, Format}; +use crate::core::{Color, Font}; + +use std::ops::Range; +use syntect::highlighting; +use syntect::parsing; + +pub struct Highlighter { + syntaxes: parsing::SyntaxSet, + syntax: parsing::SyntaxReference, + theme: highlighting::Theme, + caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, + current_line: usize, +} + +const LINES_PER_SNAPSHOT: usize = 50; + +impl highlighter::Highlighter for Highlighter { + type Settings = Settings; + type Highlight = Highlight; + + type Iterator<'a> = + Box, Self::Highlight)> + 'a>; + + fn new(settings: &Self::Settings) -> Self { + let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); + + let syntax = syntaxes + .find_syntax_by_token(&settings.extension) + .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); + + let theme = highlighting::ThemeSet::load_defaults() + .themes + .remove(settings.theme.key()) + .unwrap(); + + let parser = parsing::ParseState::new(syntax); + let stack = parsing::ScopeStack::new(); + + Highlighter { + syntax: syntax.clone(), + syntaxes, + theme, + caches: vec![(parser, stack)], + current_line: 0, + } + } + + fn update(&mut self, new_settings: &Self::Settings) { + self.syntax = self + .syntaxes + .find_syntax_by_token(&new_settings.extension) + .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()) + .clone(); + + self.theme = highlighting::ThemeSet::load_defaults() + .themes + .remove(new_settings.theme.key()) + .unwrap(); + + // Restart the highlighter + self.change_line(0); + } + + fn change_line(&mut self, line: usize) { + let snapshot = line / LINES_PER_SNAPSHOT; + + if snapshot <= self.caches.len() { + self.caches.truncate(snapshot); + self.current_line = snapshot * LINES_PER_SNAPSHOT; + } else { + self.caches.truncate(1); + self.current_line = 0; + } + + let (parser, stack) = + self.caches.last().cloned().unwrap_or_else(|| { + ( + parsing::ParseState::new(&self.syntax), + parsing::ScopeStack::new(), + ) + }); + + self.caches.push((parser, stack)); + } + + fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { + if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() { + let (parser, stack) = + self.caches.last().expect("Caches must not be empty"); + + self.caches.push((parser.clone(), stack.clone())); + } + + self.current_line += 1; + + let (parser, stack) = + self.caches.last_mut().expect("Caches must not be empty"); + + let ops = parser.parse_line(line, &self.syntaxes).unwrap_or_default(); + + let highlighter = highlighting::Highlighter::new(&self.theme); + + Box::new( + ScopeRangeIterator { + ops, + line_length: line.len(), + index: 0, + last_str_index: 0, + } + .filter_map(move |(range, scope)| { + let _ = stack.apply(&scope); + + if range.is_empty() { + None + } else { + Some(( + range, + Highlight( + highlighter.style_mod_for_stack(&stack.scopes), + ), + )) + } + }), + ) + } + + fn current_line(&self) -> usize { + self.current_line + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Settings { + pub theme: Theme, + pub extension: String, +} + +pub struct Highlight(highlighting::StyleModifier); + +impl Highlight { + pub fn color(&self) -> Option { + self.0.foreground.map(|color| { + Color::from_rgba8(color.r, color.g, color.b, color.a as f32 / 255.0) + }) + } + + pub fn font(&self) -> Option { + None + } + + pub fn to_format(&self) -> Format { + Format { + color: self.color(), + font: self.font(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Theme { + SolarizedDark, + InspiredGitHub, + Base16Mocha, +} + +impl Theme { + pub const ALL: &[Self] = + &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha]; + + fn key(&self) -> &'static str { + match self { + Theme::InspiredGitHub => "InspiredGitHub", + Theme::Base16Mocha => "base16-mocha.dark", + Theme::SolarizedDark => "Solarized (dark)", + } + } +} + +impl std::fmt::Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Theme::InspiredGitHub => write!(f, "Inspired GitHub"), + Theme::Base16Mocha => write!(f, "Mocha"), + Theme::SolarizedDark => write!(f, "Solarized Dark"), + } + } +} + +pub struct ScopeRangeIterator { + ops: Vec<(usize, parsing::ScopeStackOp)>, + line_length: usize, + index: usize, + last_str_index: usize, +} + +impl Iterator for ScopeRangeIterator { + type Item = (std::ops::Range, parsing::ScopeStackOp); + + fn next(&mut self) -> Option { + if self.index > self.ops.len() { + return None; + } + + let next_str_i = if self.index == self.ops.len() { + self.line_length + } else { + self.ops[self.index].0 + }; + + let range = self.last_str_index..next_str_i; + self.last_str_index = next_str_i; + + let op = if self.index == 0 { + parsing::ScopeStackOp::Noop + } else { + self.ops[self.index - 1].1.clone() + }; + + self.index += 1; + Some((range, op)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3cbe716a..e435a041 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -174,6 +174,9 @@ use iced_winit::runtime; pub use iced_futures::futures; +#[cfg(feature = "highlighter")] +pub use iced_highlighter as highlighter; + mod error; mod sandbox; diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs index f1c31287..f6bae7e6 100644 --- a/style/src/text_editor.rs +++ b/style/src/text_editor.rs @@ -1,6 +1,5 @@ //! Change the appearance of a text editor. -use crate::core::text::highlighter; -use crate::core::{self, Background, BorderRadius, Color}; +use crate::core::{Background, BorderRadius, Color}; /// The appearance of a text input. #[derive(Debug, Clone, Copy)] @@ -46,16 +45,3 @@ pub trait StyleSheet { /// Produces the style of a disabled text input. fn disabled(&self, style: &Self::Style) -> Appearance; } - -pub trait Highlight { - fn format(&self, theme: &Theme) -> highlighter::Format; -} - -impl Highlight for () { - fn format(&self, _theme: &Theme) -> highlighter::Format { - highlighter::Format { - color: None, - font: None, - } - } -} diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index ad12a076..c384b8a2 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -16,7 +16,7 @@ use std::cell::RefCell; use std::ops::DerefMut; use std::sync::Arc; -pub use crate::style::text_editor::{Appearance, Highlight, StyleSheet}; +pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Edit, Motion}; pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> @@ -35,6 +35,10 @@ where style: ::Style, on_edit: Option Message + 'a>>, highlighter_settings: Highlighter::Settings, + highlighter_format: fn( + &Highlighter::Highlight, + &Renderer::Theme, + ) -> highlighter::Format, } impl<'a, Message, Renderer> @@ -55,6 +59,9 @@ where style: Default::default(), on_edit: None, highlighter_settings: (), + highlighter_format: |_highlight, _theme| { + highlighter::Format::default() + }, } } } @@ -63,7 +70,6 @@ impl<'a, Highlighter, Message, Renderer> TextEditor<'a, Highlighter, Message, Renderer> where Highlighter: text::Highlighter, - Highlighter::Highlight: Highlight, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { @@ -85,6 +91,10 @@ where pub fn highlight( self, settings: H::Settings, + to_format: fn( + &H::Highlight, + &Renderer::Theme, + ) -> highlighter::Format, ) -> TextEditor<'a, H, Message, Renderer> { TextEditor { content: self.content, @@ -97,6 +107,7 @@ where style: self.style, on_edit: self.on_edit, highlighter_settings: settings, + highlighter_format: to_format, } } } @@ -203,13 +214,13 @@ struct State { drag_click: Option, highlighter: RefCell, highlighter_settings: Highlighter::Settings, + highlighter_format_address: usize, } impl<'a, Highlighter, Message, Renderer> Widget for TextEditor<'a, Highlighter, Message, Renderer> where Highlighter: text::Highlighter, - Highlighter::Highlight: Highlight, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { @@ -226,6 +237,7 @@ where &self.highlighter_settings, )), highlighter_settings: self.highlighter_settings.clone(), + highlighter_format_address: self.highlighter_format as usize, }) } @@ -246,6 +258,13 @@ where 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 @@ -354,7 +373,7 @@ where internal.editor.highlight( self.font.unwrap_or_else(|| renderer.default_font()), state.highlighter.borrow_mut().deref_mut(), - |highlight| highlight.format(theme), + |highlight| (self.highlighter_format)(highlight, theme), ); let is_disabled = self.on_edit.is_none(); @@ -458,7 +477,6 @@ impl<'a, Highlighter, Message, Renderer> for Element<'a, Message, Renderer> where Highlighter: text::Highlighter, - Highlighter::Highlight: Highlight, Message: 'a, Renderer: text::Renderer, Renderer::Theme: StyleSheet, -- cgit From 77db1699028cf50fb92b9282ffd1f73507fce974 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 20:55:39 +0200 Subject: Fix typo in `higlighter` (why is it so hard to spell?) --- highlighter/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml index 311d2998..488546c0 100644 --- a/highlighter/Cargo.toml +++ b/highlighter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_highlighter" -description = "A syntax higlighter for iced" +description = "A syntax highlighter for iced" version.workspace = true authors.workspace = true edition.workspace = true -- cgit From 01667446549d10fab18f3ca0306f278b0fe22b13 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 20:56:50 +0200 Subject: Add `iced_highlighter` to `document` workflow --- .github/workflows/document.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 230c5cb0..62e28ca3 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -15,6 +15,7 @@ jobs: RUSTDOCFLAGS="--cfg docsrs" \ cargo doc --no-deps --all-features \ -p iced_core \ + -p iced_highlighter \ -p iced_style \ -p iced_futures \ -p iced_runtime \ -- cgit From d9fbecf0d80234d63e7e5711f28fc35ee75fa503 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 20:58:15 +0200 Subject: Remove `syntect` dependency from `editor` example --- examples/editor/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index a77b1e9f..a3f6ea3b 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -12,5 +12,4 @@ iced.features = ["highlighter", "tokio", "debug"] tokio.workspace = true tokio.features = ["fs"] -syntect = "5.1" rfd = "0.12" -- cgit From a9ee8f62fdd0f74976947c21199684829aa8a496 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 21:57:09 +0200 Subject: Reuse syntaxes and themes lazily in `iced_highlighter` --- highlighter/Cargo.toml | 1 + highlighter/src/lib.rs | 51 +++++++++++++++++++++++++------------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml index 488546c0..2d108d6f 100644 --- a/highlighter/Cargo.toml +++ b/highlighter/Cargo.toml @@ -13,4 +13,5 @@ keywords.workspace = true [dependencies] iced_core.workspace = true +once_cell.workspace = true syntect.workspace = true diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index 79cfafcd..b80d6499 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -3,20 +3,26 @@ use iced_core as core; use crate::core::text::highlighter::{self, Format}; use crate::core::{Color, Font}; +use once_cell::sync::Lazy; use std::ops::Range; use syntect::highlighting; use syntect::parsing; +static SYNTAXES: Lazy = + Lazy::new(|| parsing::SyntaxSet::load_defaults_nonewlines()); + +static THEMES: Lazy = + Lazy::new(|| highlighting::ThemeSet::load_defaults()); + +const LINES_PER_SNAPSHOT: usize = 50; + pub struct Highlighter { - syntaxes: parsing::SyntaxSet, - syntax: parsing::SyntaxReference, - theme: highlighting::Theme, + syntax: &'static parsing::SyntaxReference, + highlighter: highlighting::Highlighter<'static>, caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, current_line: usize, } -const LINES_PER_SNAPSHOT: usize = 50; - impl highlighter::Highlighter for Highlighter { type Settings = Settings; type Highlight = Highlight; @@ -25,40 +31,33 @@ impl highlighter::Highlighter for Highlighter { Box, Self::Highlight)> + 'a>; fn new(settings: &Self::Settings) -> Self { - let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); - - let syntax = syntaxes + let syntax = SYNTAXES .find_syntax_by_token(&settings.extension) - .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); + .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); - let theme = highlighting::ThemeSet::load_defaults() - .themes - .remove(settings.theme.key()) - .unwrap(); + let highlighter = highlighting::Highlighter::new( + &THEMES.themes[settings.theme.key()], + ); let parser = parsing::ParseState::new(syntax); let stack = parsing::ScopeStack::new(); Highlighter { - syntax: syntax.clone(), - syntaxes, - theme, + syntax, + highlighter, caches: vec![(parser, stack)], current_line: 0, } } fn update(&mut self, new_settings: &Self::Settings) { - self.syntax = self - .syntaxes + self.syntax = SYNTAXES .find_syntax_by_token(&new_settings.extension) - .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()) - .clone(); + .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); - self.theme = highlighting::ThemeSet::load_defaults() - .themes - .remove(new_settings.theme.key()) - .unwrap(); + self.highlighter = highlighting::Highlighter::new( + &THEMES.themes[new_settings.theme.key()], + ); // Restart the highlighter self.change_line(0); @@ -99,9 +98,9 @@ impl highlighter::Highlighter for Highlighter { let (parser, stack) = self.caches.last_mut().expect("Caches must not be empty"); - let ops = parser.parse_line(line, &self.syntaxes).unwrap_or_default(); + let ops = parser.parse_line(line, &SYNTAXES).unwrap_or_default(); - let highlighter = highlighting::Highlighter::new(&self.theme); + let highlighter = &self.highlighter; Box::new( ScopeRangeIterator { -- cgit From 9af0a27e675b71164f32f8d82eb4cde9cdd459f3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 22:28:28 +0200 Subject: Draw colored glyphs in `iced_tiny_skia` --- tiny_skia/src/text.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 96cfbf32..d1b33293 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -188,7 +188,7 @@ fn draw( if let Some((buffer, placement)) = glyph_cache.allocate( physical_glyph.cache_key, - color, + glyph.color_opt.map(from_color).unwrap_or(color), font_system, &mut swash, ) { @@ -213,6 +213,12 @@ fn draw( } } +fn from_color(color: cosmic_text::Color) -> Color { + let [r, g, b, a] = color.as_rgba(); + + Color::from_rgba8(r, g, b, a as f32 / 255.0) +} + #[derive(Debug, Clone, Default)] struct GlyphCache { entries: FxHashMap< -- cgit From be340a8cd822be1ea0fe4c1b1f3a62ca66d705b4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 23:00:20 +0200 Subject: Fix gamma correction for colored glyphs in `iced_wgpu` --- core/src/color.rs | 20 ++++++++++++++++++++ graphics/src/text.rs | 12 +++++++++--- tiny_skia/src/text.rs | 14 +++++++++++++- wgpu/src/text.rs | 13 ++----------- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/core/src/color.rs b/core/src/color.rs index 1392f28b..cce8b340 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -89,6 +89,26 @@ impl Color { } } + /// Creates a [`Color`] from its linear RGBA components. + pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { + // As described in: + // https://en.wikipedia.org/wiki/SRGB + fn gamma_component(u: f32) -> f32 { + if u < 0.0031308 { + 12.92 * u + } else { + 1.055 * u.powf(1.0 / 2.4) - 0.055 + } + } + + Self { + r: gamma_component(r), + g: gamma_component(g), + b: gamma_component(b), + a, + } + } + /// Converts the [`Color`] into its RGBA8 equivalent. #[must_use] pub fn into_rgba8(self) -> [u8; 4] { diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 5fcfc699..c10eacad 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -8,6 +8,7 @@ pub use paragraph::Paragraph; pub use cosmic_text; +use crate::color; use crate::core::font::{self, Font}; use crate::core::text::Shaping; use crate::core::{Color, Size}; @@ -131,7 +132,12 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } pub fn to_color(color: Color) -> cosmic_text::Color { - let [r, g, b, a] = color.into_rgba8(); - - cosmic_text::Color::rgba(r, g, b, a) + let [r, g, b, a] = color::pack(color).components(); + + cosmic_text::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) } diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index d1b33293..70e95d01 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,6 +1,7 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; use crate::graphics::text::editor; use crate::graphics::text::font_system; @@ -216,7 +217,18 @@ fn draw( fn from_color(color: cosmic_text::Color) -> Color { let [r, g, b, a] = color.as_rgba(); - Color::from_rgba8(r, g, b, a as f32 / 255.0) + if color::GAMMA_CORRECTION { + // `cosmic_text::Color` is linear RGB in this case, so we + // need to convert back to sRGB + Color::from_linear_rgba( + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + a as f32 / 255.0, + ) + } else { + Color::from_rgba8(r, g, b, a as f32 / 255.0) + } } #[derive(Debug, Clone, Default)] diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 581df0cb..f746be63 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,7 +2,7 @@ use crate::core::alignment; use crate::core::{Rectangle, Size}; use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; -use crate::graphics::text::{font_system, Editor, Paragraph}; +use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; use crate::layer::Text; use std::borrow::Cow; @@ -214,16 +214,7 @@ impl Pipeline { right: (clip_bounds.x + clip_bounds.width) as i32, bottom: (clip_bounds.y + clip_bounds.height) as i32, }, - default_color: { - let [r, g, b, a] = color::pack(color).components(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }, + default_color: to_color(color), }) }, ); -- cgit From 93d6f748f69fc4ccf6c18f95c5f16b369c776da0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 20 Sep 2023 01:13:36 +0200 Subject: Fix `clippy` lints in `iced_highlighter` --- highlighter/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index b80d6499..f5a4fae5 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -9,10 +9,10 @@ use syntect::highlighting; use syntect::parsing; static SYNTAXES: Lazy = - Lazy::new(|| parsing::SyntaxSet::load_defaults_nonewlines()); + Lazy::new(parsing::SyntaxSet::load_defaults_nonewlines); static THEMES: Lazy = - Lazy::new(|| highlighting::ThemeSet::load_defaults()); + Lazy::new(highlighting::ThemeSet::load_defaults); const LINES_PER_SNAPSHOT: usize = 50; @@ -77,7 +77,7 @@ impl highlighter::Highlighter for Highlighter { let (parser, stack) = self.caches.last().cloned().unwrap_or_else(|| { ( - parsing::ParseState::new(&self.syntax), + parsing::ParseState::new(self.syntax), parsing::ScopeStack::new(), ) }); -- cgit From ff78e97ad7df4db3b2a97b94e99854f2f9e3021a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 20 Sep 2023 01:21:42 +0200 Subject: Introduce more themes to `iced_highlighter` --- examples/editor/src/main.rs | 6 +++++- highlighter/src/lib.rs | 35 ++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index d513090f..f49ca6e8 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -230,7 +230,11 @@ impl Application for Editor { } fn theme(&self) -> Theme { - Theme::Dark + if self.theme.is_dark() { + Theme::Dark + } else { + Theme::Light + } } } diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index f5a4fae5..db28b5b1 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -161,19 +161,38 @@ impl Highlight { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Theme { SolarizedDark, - InspiredGitHub, Base16Mocha, + Base16Ocean, + Base16Eighties, + InspiredGitHub, } impl Theme { - pub const ALL: &[Self] = - &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha]; + pub const ALL: &[Self] = &[ + Self::SolarizedDark, + Self::Base16Mocha, + Self::Base16Ocean, + Self::Base16Eighties, + Self::InspiredGitHub, + ]; + + pub fn is_dark(self) -> bool { + match self { + Self::SolarizedDark + | Self::Base16Mocha + | Self::Base16Ocean + | Self::Base16Eighties => true, + Self::InspiredGitHub => false, + } + } fn key(&self) -> &'static str { match self { - Theme::InspiredGitHub => "InspiredGitHub", - Theme::Base16Mocha => "base16-mocha.dark", Theme::SolarizedDark => "Solarized (dark)", + Theme::Base16Mocha => "base16-mocha.dark", + Theme::Base16Ocean => "base16-ocean.dark", + Theme::Base16Eighties => "base16-eighties.dark", + Theme::InspiredGitHub => "InspiredGitHub", } } } @@ -181,9 +200,11 @@ impl Theme { impl std::fmt::Display for Theme { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Theme::InspiredGitHub => write!(f, "Inspired GitHub"), - Theme::Base16Mocha => write!(f, "Mocha"), Theme::SolarizedDark => write!(f, "Solarized Dark"), + Theme::Base16Mocha => write!(f, "Mocha"), + Theme::Base16Ocean => write!(f, "Ocean"), + Theme::Base16Eighties => write!(f, "Eighties"), + Theme::InspiredGitHub => write!(f, "Inspired GitHub"), } } } -- cgit From 29fb4eab878a7ba399cae6ab1ec18a71e369ee59 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 20 Sep 2023 01:23:50 +0200 Subject: Scroll `TextEditor` only if `cursor.is_over(bounds)` --- widget/src/text_editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index c384b8a2..4191e02c 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -539,7 +539,9 @@ impl Update { } _ => None, }, - mouse::Event::WheelScrolled { delta } => { + mouse::Event::WheelScrolled { delta } + if cursor.is_over(bounds) => + { action(Action::Scroll { lines: match delta { mouse::ScrollDelta::Lines { y, .. } => { -- cgit From 25d47c3238ce23854e2c78e2bd9ad2b1f4b326b3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 21 Sep 2023 06:05:46 +0200 Subject: Remove `rangemap` patch in `Cargo.toml` --- Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8899fa67..77d4c647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,7 +163,3 @@ winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8 [patch.crates-io.cosmic-text] git = "https://github.com/hecrj/cosmic-text.git" branch = "respect-fontconfig-aliases" - -[patch.crates-io.rangemap] -git = "https://github.com/hecrj/rangemap.git" -branch = "fix/partial-eq" -- cgit From da5dd2526a2d9ee27e9405ed19c0f7a641160c54 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 21 Sep 2023 06:07:19 +0200 Subject: Round `ScrollDelta::Lines` in `TextEditor` --- widget/src/text_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 4191e02c..ac927fbc 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -545,7 +545,7 @@ impl Update { action(Action::Scroll { lines: match delta { mouse::ScrollDelta::Lines { y, .. } => { - -y as i32 * 4 + -y.round() as i32 * 4 } mouse::ScrollDelta::Pixels { y, .. } => { -y.signum() as i32 -- cgit From 7373dd856b8837c2d91067b45e43b8f0e767c917 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 21 Sep 2023 06:13:08 +0200 Subject: Scroll at least one line on macOS in `TextEditor` --- widget/src/text_editor.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index ac927fbc..76f3cc18 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -545,7 +545,11 @@ impl Update { action(Action::Scroll { lines: match delta { mouse::ScrollDelta::Lines { y, .. } => { - -y.round() as i32 * 4 + if y > 0.0 { + -(y * 4.0).min(1.0) as i32 + } else { + 0 + } } mouse::ScrollDelta::Pixels { y, .. } => { -y.signum() as i32 -- cgit From 68d49459ce0e8b28e56b71970cb26e66ac1b01b4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 21 Sep 2023 06:17:47 +0200 Subject: Fix vertical scroll for `TextEditor` --- widget/src/text_editor.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 76f3cc18..e8187b9c 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -545,8 +545,9 @@ impl Update { action(Action::Scroll { lines: match delta { mouse::ScrollDelta::Lines { y, .. } => { - if y > 0.0 { - -(y * 4.0).min(1.0) as i32 + if y.abs() > 0.0 { + (y.signum() * -(y.abs() * 4.0).max(1.0)) + as i32 } else { 0 } -- cgit From 70e49df4289b925d24f92ce5c91ef2b03dbc54e3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 22 Sep 2023 05:50:31 +0200 Subject: Fix selection clipping out of bounds in `TextEditor` --- widget/src/text_editor.rs | 57 +++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index e8187b9c..c142c22d 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -406,38 +406,47 @@ where style.text_color, ); + let translation = Vector::new( + bounds.x + self.padding.left, + bounds.y + self.padding.top, + ); + if state.is_focused { match internal.editor.cursor() { Cursor::Caret(position) => { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: position.x + bounds.x + self.padding.left, - y: position.y + bounds.y + self.padding.top, - width: 1.0, - height: self - .line_height - .to_absolute(self.text_size.unwrap_or_else( - || renderer.default_size(), - )) - .into(), + let position = position + translation; + + if bounds.contains(position) { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: position.x, + y: position.y, + width: 1.0, + height: self + .line_height + .to_absolute( + self.text_size.unwrap_or_else( + || renderer.default_size(), + ), + ) + .into(), + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - theme.value_color(&self.style), - ); + theme.value_color(&self.style), + ); + } } Cursor::Selection(ranges) => { - for range in ranges { + for range in ranges.into_iter().filter_map(|range| { + bounds.intersection(&(range + translation)) + }) { renderer.fill_quad( renderer::Quad { - bounds: range - + Vector::new( - bounds.x + self.padding.left, - bounds.y + self.padding.top, - ), + bounds: range, border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, -- cgit From af21cf82492bf7ffa1241cebae182c5916fc07d1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 22 Sep 2023 05:55:27 +0200 Subject: Remove `patch.crates-io` section for `cosmic-text` in `Cargo.toml` --- Cargo.toml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 77d4c647..888e2df8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,10 +121,10 @@ iced_winit = { version = "0.12", path = "winit" } async-std = "1.0" bitflags = "1.0" bytemuck = { version = "1.0", features = ["derive"] } -cosmic-text = "0.9" +cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git", rev = "30398c2f0cb79267d440870bc47967579e31a2ae" } futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "20f0f8fa80e0d0df4c63634ce9176fa489546ca9" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "0a8366be5ec6d48c3e10c996ba840936992d878f" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -159,7 +159,3 @@ wgpu = "0.17" winapi = "0.3" window_clipboard = "0.3" winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false } - -[patch.crates-io.cosmic-text] -git = "https://github.com/hecrj/cosmic-text.git" -branch = "respect-fontconfig-aliases" -- cgit From 8cc19de254c37d3123d5ea1b6513f1f34d35c7c8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 22 Sep 2023 06:00:51 +0200 Subject: Add `text` helper method for `text_editor::Content` --- examples/editor/src/main.rs | 19 +------------------ widget/src/text_editor.rs | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index f49ca6e8..a69e1f54 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -114,25 +114,8 @@ impl Application for Editor { } else { self.is_loading = true; - let mut contents = self.content.lines().enumerate().fold( - String::new(), - |mut contents, (i, line)| { - if i > 0 { - contents.push('\n'); - } - - contents.push_str(&line); - - contents - }, - ); - - if !contents.ends_with('\n') { - contents.push('\n'); - } - Command::perform( - save_file(self.file.clone(), contents), + save_file(self.file.clone(), self.content.text()), Message::FileSaved, ) } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index c142c22d..6d25967e 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -190,6 +190,27 @@ where } } + pub fn text(&self) -> String { + let mut text = self.lines().enumerate().fold( + String::new(), + |mut contents, (i, line)| { + if i > 0 { + contents.push('\n'); + } + + contents.push_str(&line); + + contents + }, + ); + + if !text.ends_with('\n') { + text.push('\n'); + } + + text + } + pub fn selection(&self) -> Option { self.0.borrow().editor.selection() } -- cgit From 625cd745f38215b1cb8f629cdc6d2fa41c9a739a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 27 Oct 2023 05:04:14 +0200 Subject: Write documentation for the new text APIs --- core/src/lib.rs | 2 +- core/src/mouse/click.rs | 1 + core/src/text.rs | 2 ++ core/src/text/editor.rs | 51 +++++++++++++++++++++++++++++++++++++++++- core/src/text/highlighter.rs | 30 ++++++++++++++++++++----- examples/editor/src/main.rs | 10 ++++----- graphics/src/lib.rs | 2 +- graphics/src/text.rs | 13 +++++++++++ graphics/src/text/cache.rs | 19 ++++++++++++++++ graphics/src/text/editor.rs | 12 ++++++++++ graphics/src/text/paragraph.rs | 14 ++++++++++++ style/src/lib.rs | 2 +- wgpu/src/layer/text.rs | 7 +++++- wgpu/src/lib.rs | 2 +- widget/src/lib.rs | 4 ++-- widget/src/text_editor.rs | 36 +++++++++++++++++++++++++---- 16 files changed, 185 insertions(+), 22 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index 9eb3da34..54ea5839 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -12,7 +12,7 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![deny( missing_debug_implementations, - // missing_docs, + missing_docs, unused_results, rustdoc::broken_intra_doc_links )] diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index b427da6c..6f3844be 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -61,6 +61,7 @@ impl Click { self.kind } + /// Returns the position of the [`Click`]. pub fn position(&self) -> Point { self.position } diff --git a/core/src/text.rs b/core/src/text.rs index 9b9c753c..546d0b5c 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -204,6 +204,8 @@ pub trait Renderer: crate::Renderer { color: Color, ); + /// Draws the given [`Editor`] at the given position and with the given + /// [`Color`]. fn fill_editor( &mut self, editor: &Self::Editor, diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index e9d66ce9..ebb0eee2 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -1,25 +1,36 @@ +//! Edit text. use crate::text::highlighter::{self, Highlighter}; use crate::text::LineHeight; use crate::{Pixels, Point, Rectangle, Size}; use std::sync::Arc; +/// A component that can be used by widgets to edit multi-line text. pub trait Editor: Sized + Default { + /// The [`Font`] of the [`Editor`]. type Font: Copy + PartialEq + Default; /// Creates a new [`Editor`] laid out with the given text. fn with_text(text: &str) -> Self; + /// Returns the current [`Cursor`] of the [`Editor`]. fn cursor(&self) -> Cursor; + /// Returns the current cursor position of the [`Editor`]. + /// + /// Line and column, respectively. fn cursor_position(&self) -> (usize, usize); + /// Returns the current selected text of the [`Editor`]. fn selection(&self) -> Option; + /// Returns the text of the given line in the [`Editor`], if it exists. fn line(&self, index: usize) -> Option<&str>; + /// Returns the amount of lines in the [`Editor`]. fn line_count(&self) -> usize; + /// Performs an [`Action`] on the [`Editor`]. fn perform(&mut self, action: Action); /// Returns the current boundaries of the [`Editor`]. @@ -35,6 +46,7 @@ pub trait Editor: Sized + Default { new_highlighter: &mut impl Highlighter, ); + /// Runs a text [`Highlighter`] in the [`Editor`]. fn highlight( &mut self, font: Self::Font, @@ -43,50 +55,83 @@ pub trait Editor: Sized + Default { ); } +/// An interaction with an [`Editor`]. #[derive(Debug, Clone, PartialEq)] pub enum Action { + /// 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, + /// Perform an [`Edit`]. Edit(Edit), + /// Click the [`Editor`] at the given [`Point`]. Click(Point), + /// Drag the mouse on the [`Editor`] to the given [`Point`]. Drag(Point), - Scroll { lines: i32 }, + /// Scroll the [`Editor`] a certain amount of lines. + Scroll { + /// The amount of lines to scroll. + lines: i32, + }, } impl Action { + /// Returns whether the [`Action`] is an editing action. pub fn is_edit(&self) -> bool { matches!(self, Self::Edit(_)) } } +/// An action that edits text. #[derive(Debug, Clone, PartialEq)] pub enum Edit { + /// Insert the given character. Insert(char), + /// Paste the given text. Paste(Arc), + /// Break the current line. Enter, + /// Delete the previous character. Backspace, + /// Delete the next character. Delete, } +/// A cursor movement. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Motion { + /// Move left. Left, + /// Move right. Right, + /// Move up. Up, + /// Move down. Down, + /// Move to the left boundary of a word. WordLeft, + /// Move to the right boundary of a word. WordRight, + /// Move to the start of the line. Home, + /// Move to the end of the line. End, + /// Move to the start of the previous window. PageUp, + /// Move to the start of the next window. PageDown, + /// Move to the start of the text. DocumentStart, + /// Move to the end of the text. DocumentEnd, } impl Motion { + /// Widens the [`Motion`], if possible. pub fn widen(self) -> Self { match self { Self::Left => Self::WordLeft, @@ -97,6 +142,7 @@ impl Motion { } } + /// Returns the [`Direction`] of the [`Motion`]. pub fn direction(&self) -> Direction { match self { Self::Left @@ -115,9 +161,12 @@ impl Motion { } } +/// A direction in some text. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Direction { + /// <- Left, + /// -> Right, } diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs index 9a9cff89..a0535228 100644 --- a/core/src/text/highlighter.rs +++ b/core/src/text/highlighter.rs @@ -1,31 +1,48 @@ +//! Highlight text. use crate::Color; use std::ops::Range; +/// A type capable of highlighting text. +/// +/// A [`Highlighter`] highlights lines in sequence. When a line changes, +/// it must be notified and the lines after the changed one must be fed +/// again to the [`Highlighter`]. pub trait Highlighter: 'static { + /// The settings to configure the [`Highlighter`]. type Settings: PartialEq + Clone; + + /// The output of the [`Highlighter`]. type Highlight; + /// The highlight iterator type. type Iterator<'a>: Iterator, Self::Highlight)> where Self: 'a; + /// Creates a new [`Highlighter`] from its [`Self::Settings`]. fn new(settings: &Self::Settings) -> Self; + /// Updates the [`Highlighter`] with some new [`Self::Settings`]. fn update(&mut self, new_settings: &Self::Settings); + /// Notifies the [`Highlighter`] that the line at the given index has changed. fn change_line(&mut self, line: usize); + /// Highlights the given line. + /// + /// If a line changed prior to this, the first line provided here will be the + /// line that changed. fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>; + /// Returns the current line of the [`Highlighter`]. + /// + /// If `change_line` has been called, this will normally be the least index + /// that changed. fn current_line(&self) -> usize; } -#[derive(Debug, Clone, Copy)] -pub struct Style { - pub color: Color, -} - +/// A highlighter that highlights nothing. #[derive(Debug, Clone, Copy)] pub struct PlainText; @@ -52,9 +69,12 @@ impl Highlighter for PlainText { } } +/// The format of some text. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Format { + /// The [`Color`] of the text. pub color: Option, + /// The `Font` of the text. pub font: Option, } diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index a69e1f54..03d1e283 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -34,7 +34,7 @@ struct Editor { #[derive(Debug, Clone)] enum Message { - Edit(text_editor::Action), + ActionPerformed(text_editor::Action), ThemeSelected(highlighter::Theme), NewFile, OpenFile, @@ -68,10 +68,10 @@ impl Application for Editor { fn update(&mut self, message: Message) -> Command { match message { - Message::Edit(action) => { + Message::ActionPerformed(action) => { self.is_dirty = self.is_dirty || action.is_edit(); - self.content.edit(action); + self.content.perform(action); Command::none() } @@ -103,7 +103,7 @@ impl Application for Editor { if let Ok((path, contents)) = result { self.file = Some(path); - self.content = text_editor::Content::with(&contents); + self.content = text_editor::Content::with_text(&contents); } Command::none() @@ -191,7 +191,7 @@ impl Application for Editor { column![ controls, text_editor(&self.content) - .on_edit(Message::Edit) + .on_action(Message::ActionPerformed) .highlight::( highlighter::Settings { theme: self.theme, diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index a0729058..7a213909 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -10,7 +10,7 @@ #![forbid(rust_2018_idioms)] #![deny( missing_debug_implementations, - //missing_docs, + missing_docs, unsafe_code, unused_results, rustdoc::broken_intra_doc_links diff --git a/graphics/src/text.rs b/graphics/src/text.rs index c10eacad..7261900e 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -1,3 +1,4 @@ +//! Draw text. pub mod cache; pub mod editor; pub mod paragraph; @@ -17,6 +18,7 @@ use once_cell::sync::OnceCell; use std::borrow::Cow; use std::sync::{Arc, RwLock}; +/// Returns the global [`FontSystem`]. pub fn font_system() -> &'static RwLock { static FONT_SYSTEM: OnceCell> = OnceCell::new(); @@ -32,6 +34,7 @@ pub fn font_system() -> &'static RwLock { }) } +/// A set of system fonts. #[allow(missing_debug_implementations)] pub struct FontSystem { raw: cosmic_text::FontSystem, @@ -39,10 +42,12 @@ pub struct FontSystem { } impl FontSystem { + /// Returns the raw [`cosmic_text::FontSystem`]. pub fn raw(&mut self) -> &mut cosmic_text::FontSystem { &mut self.raw } + /// Loads a font from its bytes. pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { let _ = self.raw.db_mut().load_font_source( cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), @@ -51,14 +56,19 @@ impl FontSystem { self.version = Version(self.version.0 + 1); } + /// Returns the current [`Version`] of the [`FontSystem`]. + /// + /// Loading a font will increase the version of a [`FontSystem`]. pub fn version(&self) -> Version { self.version } } +/// A version number. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Version(u32); +/// Measures the dimensions of the given [`cosmic_text::Buffer`]. pub fn measure(buffer: &cosmic_text::Buffer) -> Size { let (width, total_lines) = buffer .layout_runs() @@ -69,6 +79,7 @@ pub fn measure(buffer: &cosmic_text::Buffer) -> Size { Size::new(width, total_lines as f32 * buffer.metrics().line_height) } +/// Returns the attributes of the given [`Font`]. pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> { cosmic_text::Attrs::new() .family(to_family(font.family)) @@ -124,6 +135,7 @@ fn to_style(style: font::Style) -> cosmic_text::Style { } } +/// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy. pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { match shaping { Shaping::Basic => cosmic_text::Shaping::Basic, @@ -131,6 +143,7 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } } +/// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { let [r, g, b, a] = color::pack(color).components(); diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 577c4687..b3293dd4 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -1,3 +1,4 @@ +//! Cache text. use crate::core::{Font, Size}; use crate::text; @@ -5,6 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::hash_map; use std::hash::{BuildHasher, Hash, Hasher}; +/// A store of recently used sections of text. #[allow(missing_debug_implementations)] #[derive(Default)] pub struct Cache { @@ -21,14 +23,17 @@ type HashBuilder = twox_hash::RandomXxHashBuilder64; type HashBuilder = std::hash::BuildHasherDefault; impl Cache { + /// Creates a new empty [`Cache`]. pub fn new() -> Self { Self::default() } + /// Gets the text [`Entry`] with the given [`KeyHash`]. pub fn get(&self, key: &KeyHash) -> Option<&Entry> { self.entries.get(key) } + /// Allocates a text [`Entry`] if it is not already present in the [`Cache`]. pub fn allocate( &mut self, font_system: &mut cosmic_text::FontSystem, @@ -88,6 +93,9 @@ impl Cache { (hash, self.entries.get_mut(&hash).unwrap()) } + /// Trims the [`Cache`]. + /// + /// This will clear the sections of text that have not been used since the last `trim`. pub fn trim(&mut self) { self.entries .retain(|key, _| self.recently_used.contains(key)); @@ -99,13 +107,20 @@ impl Cache { } } +/// A cache key representing a section of text. #[derive(Debug, Clone, Copy)] pub struct Key<'a> { + /// The content of the text. pub content: &'a str, + /// The size of the text. pub size: f32, + /// The line height of the text. pub line_height: f32, + /// The [`Font`] of the text. pub font: Font, + /// The bounds of the text. pub bounds: Size, + /// The shaping strategy of the text. pub shaping: text::Shaping, } @@ -123,10 +138,14 @@ impl Key<'_> { } } +/// The hash of a [`Key`]. pub type KeyHash = u64; +/// A cache entry. #[allow(missing_debug_implementations)] pub struct Entry { + /// The buffer of text, ready for drawing. pub buffer: cosmic_text::Buffer, + /// The minimum bounds of the text. pub min_bounds: Size, } diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index a05312dc..d5262ae8 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,3 +1,4 @@ +//! Draw and edit text. use crate::core::text::editor::{ self, Action, Cursor, Direction, Edit, Motion, }; @@ -11,6 +12,7 @@ use cosmic_text::Edit as _; use std::fmt; use std::sync::{self, Arc}; +/// A multi-line text editor. #[derive(Debug, PartialEq)] pub struct Editor(Option>); @@ -23,14 +25,21 @@ struct Internal { } impl Editor { + /// Creates a new empty [`Editor`]. pub fn new() -> Self { Self::default() } + /// Returns the buffer of the [`Editor`]. pub fn buffer(&self) -> &cosmic_text::Buffer { self.internal().editor.buffer() } + /// Creates a [`Weak`] reference to the [`Editor`]. + /// + /// This is useful to avoid cloning the [`Editor`] when + /// referential guarantees are unnecessary. For instance, + /// when creating a rendering tree. pub fn downgrade(&self) -> Weak { let editor = self.internal(); @@ -662,13 +671,16 @@ impl fmt::Debug for Internal { } } +/// A weak reference to an [`Editor`]. #[derive(Debug, Clone)] pub struct Weak { raw: sync::Weak, + /// The bounds of the [`Editor`]. pub bounds: Size, } impl Weak { + /// Tries to update the reference into an [`Editor`]. pub fn upgrade(&self) -> Option { self.raw.upgrade().map(Some).map(Editor) } diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index d0396e8e..ccfe4a61 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,3 +1,4 @@ +//! Draw paragraphs. use crate::core; use crate::core::alignment; use crate::core::text::{Hit, LineHeight, Shaping, Text}; @@ -7,6 +8,7 @@ use crate::text; use std::fmt; use std::sync::{self, Arc}; +/// A bunch of text. #[derive(Clone, PartialEq)] pub struct Paragraph(Option>); @@ -23,14 +25,21 @@ struct Internal { } impl Paragraph { + /// Creates a new empty [`Paragraph`]. pub fn new() -> Self { Self::default() } + /// Returns the buffer of the [`Paragraph`]. pub fn buffer(&self) -> &cosmic_text::Buffer { &self.internal().buffer } + /// Creates a [`Weak`] reference to the [`Paragraph`]. + /// + /// This is useful to avoid cloning the [`Editor`] when + /// referential guarantees are unnecessary. For instance, + /// when creating a rendering tree. pub fn downgrade(&self) -> Weak { let paragraph = self.internal(); @@ -269,15 +278,20 @@ impl Default for Internal { } } +/// A weak reference to a [`Paragraph`]. #[derive(Debug, Clone)] pub struct Weak { raw: sync::Weak, + /// The minimum bounds of the [`Paragraph`]. pub min_bounds: Size, + /// The horizontal alignment of the [`Paragraph`]. pub horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the [`Paragraph`]. pub vertical_alignment: alignment::Vertical, } impl Weak { + /// Tries to update the reference into a [`Paragraph`]. pub fn upgrade(&self) -> Option { self.raw.upgrade().map(Some).map(Paragraph) } diff --git a/style/src/lib.rs b/style/src/lib.rs index 35460f4b..e4097434 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -10,7 +10,7 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![deny( unused_results, - // missing_docs, + missing_docs, unused_results, rustdoc::broken_intra_doc_links )] diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index d46b39da..66417cec 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -4,19 +4,24 @@ use crate::core::{Color, Font, Pixels, Point, Rectangle}; use crate::graphics::text::editor; use crate::graphics::text::paragraph; -/// A paragraph of text. +/// A text primitive. #[derive(Debug, Clone)] pub enum Text<'a> { + /// A paragraph. + #[allow(missing_docs)] Paragraph { paragraph: paragraph::Weak, position: Point, color: Color, }, + /// An editor. + #[allow(missing_docs)] Editor { editor: editor::Weak, position: Point, color: Color, }, + /// A cached text. Cached(Cached<'a>), } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 6d26723e..424dfeb3 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -23,7 +23,7 @@ #![forbid(rust_2018_idioms)] #![deny( missing_debug_implementations, - //missing_docs, + missing_docs, unsafe_code, unused_results, rustdoc::broken_intra_doc_links diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 97e4ac58..e3335a98 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -4,8 +4,8 @@ )] #![forbid(unsafe_code, rust_2018_idioms)] #![deny( - // missing_debug_implementations, - // missing_docs, + //missing_debug_implementations, + missing_docs, unused_results, rustdoc::broken_intra_doc_links )] diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 6d25967e..da1905dc 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1,3 +1,4 @@ +//! Display a multi-line text input for text editing. use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout::{self, Layout}; @@ -19,6 +20,7 @@ use std::sync::Arc; pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Edit, Motion}; +/// A multi-line text input. pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> where Highlighter: text::Highlighter, @@ -47,6 +49,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { + /// Creates new [`TextEditor`] with the given [`Content`]. pub fn new(content: &'a Content) -> Self { Self { content, @@ -73,21 +76,34 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - pub fn on_edit(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> 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`]. + /// + /// [`Font`]: text::Renderer::Font pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); self } + /// Sets the [`Padding`] of the [`TextEditor`]. pub fn padding(mut self, padding: impl Into) -> Self { self.padding = padding.into(); self } + /// Highlights the [`TextEditor`] with the given [`Highlighter`] and + /// a strategy to turn its highlights into some text format. pub fn highlight( self, settings: H::Settings, @@ -112,6 +128,7 @@ where } } +/// The content of a [`TextEditor`]. pub struct Content(RefCell>) where R: text::Renderer; @@ -128,28 +145,33 @@ impl Content where R: text::Renderer, { + /// Creates an empty [`Content`]. pub fn new() -> Self { - Self::with("") + Self::with_text("") } - pub fn with(text: &str) -> Self { + /// Creates a [`Content`] with the given text. + pub fn with_text(text: &str) -> Self { Self(RefCell::new(Internal { editor: R::Editor::with_text(text), is_dirty: true, })) } - pub fn edit(&mut self, action: Action) { + /// Performs an [`Action`] on the [`Content`]. + pub fn perform(&mut self, action: Action) { let internal = self.0.get_mut(); internal.editor.perform(action); internal.is_dirty = true; } + /// 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, @@ -160,6 +182,7 @@ where .ok() } + /// Returns an iterator of the text of the lines in the [`Content`]. pub fn lines( &self, ) -> impl Iterator + '_> { @@ -190,6 +213,9 @@ where } } + /// Returns the text of the [`Content`]. + /// + /// Lines are joined with `'\n'`. pub fn text(&self) -> String { let mut text = self.lines().enumerate().fold( String::new(), @@ -211,10 +237,12 @@ where text } + /// Returns the selected text of the [`Content`]. pub fn selection(&self) -> Option { self.0.borrow().editor.selection() } + /// Returns the current cursor position of the [`Content`]. pub fn cursor_position(&self) -> (usize, usize) { self.0.borrow().editor.cursor_position() } -- cgit From e579d8553088c7d17784e7ff8f6e21360c2bd9ef Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 27 Oct 2023 05:08:06 +0200 Subject: Implement missing debug implementations in `iced_widget` --- widget/src/lib.rs | 2 +- widget/src/text_editor.rs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/widget/src/lib.rs b/widget/src/lib.rs index e3335a98..2f959370 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -4,7 +4,7 @@ )] #![forbid(unsafe_code, rust_2018_idioms)] #![deny( - //missing_debug_implementations, + missing_debug_implementations, missing_docs, unused_results, rustdoc::broken_intra_doc_links diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index da1905dc..ac24920f 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -14,6 +14,7 @@ use crate::core::{ }; use std::cell::RefCell; +use std::fmt; use std::ops::DerefMut; use std::sync::Arc; @@ -21,6 +22,7 @@ pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Edit, Motion}; /// A multi-line text input. +#[allow(missing_debug_implementations)] pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> where Highlighter: text::Highlighter, @@ -257,6 +259,21 @@ where } } +impl fmt::Debug for Content +where + Renderer: text::Renderer, + Renderer::Editor: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let internal = self.0.borrow(); + + f.debug_struct("Content") + .field("editor", &internal.editor) + .field("is_dirty", &internal.is_dirty) + .finish() + } +} + struct State { is_focused: bool, last_click: Option, -- cgit From 57f9024e89256ad3f99a3ab19bdc8524c1defa54 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 27 Oct 2023 05:19:35 +0200 Subject: Fix intra-doc broken links --- .cargo/config.toml | 2 -- core/src/text/editor.rs | 2 +- graphics/src/text/paragraph.rs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 3e02dda8..85a46cda 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -17,8 +17,6 @@ clippy --workspace --no-deps -- \ -D clippy::useless_conversion """ -#![allow(clippy::inherent_to_string, clippy::type_complexity)] - nitpick = """ clippy --workspace --no-deps -- \ -D warnings \ diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index ebb0eee2..f3c6e342 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -7,7 +7,7 @@ use std::sync::Arc; /// A component that can be used by widgets to edit multi-line text. pub trait Editor: Sized + Default { - /// The [`Font`] of the [`Editor`]. + /// The font of the [`Editor`]. type Font: Copy + PartialEq + Default; /// Creates a new [`Editor`] laid out with the given text. diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index ccfe4a61..4a08a8f4 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -37,7 +37,7 @@ impl Paragraph { /// Creates a [`Weak`] reference to the [`Paragraph`]. /// - /// This is useful to avoid cloning the [`Editor`] when + /// This is useful to avoid cloning the [`Paragraph`] when /// referential guarantees are unnecessary. For instance, /// when creating a rendering tree. pub fn downgrade(&self) -> Weak { -- cgit From c8eca4e6bfae82013e6bb08e9d8bf66560b36564 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 27 Oct 2023 16:37:58 +0200 Subject: Improve `TextEditor` scroll interaction with a touchpad --- widget/src/text_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index ac24920f..1708a2e5 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -628,7 +628,7 @@ impl Update { } } mouse::ScrollDelta::Pixels { y, .. } => { - -y.signum() as i32 + (-y / 4.0) as i32 } }, }) -- cgit