Merge pull request #2123 from iced-rs/text-editor

`TextEditor` widget (or multi-line text input)
This commit is contained in:
Héctor Ramón 2023-10-27 17:36:54 +02:00 committed by GitHub
commit d731996342
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 3142 additions and 424 deletions

View file

@ -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 \

View file

@ -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 \

View file

@ -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:

View file

@ -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

View file

@ -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"

View file

@ -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] {

View file

@ -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
};

View file

@ -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,

View file

@ -61,6 +61,11 @@ impl Click {
self.kind
}
/// Returns the position of the [`Click`].
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)

View file

@ -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';
@ -58,16 +59,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,
@ -76,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>,
@ -88,24 +87,12 @@ impl text::Renderer for Null {
impl text::Paragraph for () {
type Font = Font;
fn content(&self) -> &str {
""
}
fn with_text(_text: Text<'_, Self::Font>) -> Self {}
fn text_size(&self) -> Pixels {
Pixels(16.0)
}
fn resize(&mut self, _new_bounds: Size) {}
fn font(&self) -> Self::Font {
Font::default()
}
fn line_height(&self) -> text::LineHeight {
text::LineHeight::default()
}
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 +107,6 @@ impl text::Paragraph for () {
None
}
fn bounds(&self) -> Size {
Size::ZERO
}
fn min_bounds(&self) -> Size {
Size::ZERO
}
@ -132,3 +115,55 @@ 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 cursor_position(&self) -> (usize, usize) {
(0, 0)
}
fn selection(&self) -> Option<String> {
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 {
Size::ZERO
}
fn update(
&mut self,
_new_bounds: Size,
_new_font: Self::Font,
_new_size: Pixels,
_new_line_height: text::LineHeight,
_new_highlighter: &mut impl text::Highlighter,
) {
}
fn highlight<H: text::Highlighter>(
&mut self,
_font: Self::Font,
_highlighter: &mut H,
_format_highlight: impl Fn(
&H::Highlight,
) -> text::highlighter::Format<Self::Font>,
) {
}
}

View file

@ -1,4 +1,13 @@
//! Draw and interact with text.
mod paragraph;
pub mod editor;
pub mod highlighter;
pub use editor::Editor;
pub use highlighter::Highlighter;
pub use paragraph::Paragraph;
use crate::alignment;
use crate::{Color, Pixels, Point, Size};
@ -126,6 +135,33 @@ impl Hit {
}
}
/// The difference detected in some text.
///
/// 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.
///
/// 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 +170,9 @@ pub trait Renderer: crate::Renderer {
/// The [`Paragraph`] of this [`Renderer`].
type Paragraph: Paragraph<Font = Self::Font> + 'static;
/// The [`Editor`] of this [`Renderer`].
type Editor: Editor<Font = Self::Font> + 'static;
/// The icon font of the backend.
const ICON_FONT: Self::Font;
@ -156,33 +195,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(
@ -192,6 +204,15 @@ 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,
position: Point,
color: Color,
);
/// Draws the given [`Text`] at the given position and with the given
/// [`Color`].
fn fill_text(
@ -201,101 +222,3 @@ pub trait Renderer: crate::Renderer {
color: Color,
);
}
/// A text paragraph.
pub trait Paragraph: 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;
/// Returns the [`LineHeight`] of the [`Paragraph`].
fn line_height(&self) -> LineHeight;
/// Returns the [`Self::Font`] of the [`Paragraph`].
fn font(&self) -> Self::Font;
/// Returns the [`Shaping`] strategy of the [`Paragraph`].
fn shaping(&self) -> Shaping;
/// 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 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;
/// 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<Hit>;
/// Returns the distance to the given grapheme index in the [`Paragraph`].
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
/// 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,
}
/// Compares a [`Paragraph`] with some desired [`Text`] and returns the
/// [`Difference`].
pub fn compare<Font: PartialEq>(
paragraph: &impl Paragraph<Font = Font>,
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
}
}

181
core/src/text/editor.rs Normal file
View file

@ -0,0 +1,181 @@
//! 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<String>;
/// 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`].
fn 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,
new_highlighter: &mut impl Highlighter,
);
/// Runs a text [`Highlighter`] in the [`Editor`].
fn highlight<H: Highlighter>(
&mut self,
font: Self::Font,
highlighter: &mut H,
format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>,
);
}
/// 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 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<String>),
/// 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,
Self::Right => Self::WordRight,
Self::Home => Self::DocumentStart,
Self::End => Self::DocumentEnd,
_ => self,
}
}
/// Returns the [`Direction`] of the [`Motion`].
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,
}
}
}
/// A direction in some text.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
/// <-
Left,
/// ->
Right,
}
/// 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<Rectangle>),
}

View file

@ -0,0 +1,88 @@
//! 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<Item = (Range<usize>, 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;
}
/// A highlighter that highlights nothing.
#[derive(Debug, Clone, Copy)]
pub struct PlainText;
impl Highlighter for PlainText {
type Settings = ();
type Highlight = ();
type Iterator<'a> = std::iter::Empty<(Range<usize>, Self::Highlight)>;
fn new(_settings: &Self::Settings) -> Self {
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<'_> {
std::iter::empty()
}
fn current_line(&self) -> usize {
usize::MAX
}
}
/// The format of some text.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Format<Font> {
/// The [`Color`] of the text.
pub color: Option<Color>,
/// The `Font` of the text.
pub font: Option<Font>,
}
impl<Font> Default for Format<Font> {
fn default() -> Self {
Self {
color: None,
font: None,
}
}
}

View file

@ -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<Hit>;
/// Returns the distance to the given grapheme index in the [`Paragraph`].
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
/// 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
}
}

View file

@ -212,19 +212,16 @@ where
let State(ref mut paragraph) = state;
renderer.update_paragraph(
paragraph,
text::Text {
content,
bounds,
size,
line_height,
font,
horizontal_alignment,
vertical_alignment,
shaping,
},
);
paragraph.update(text::Text {
content,
bounds,
size,
line_height,
font,
horizontal_alignment,
vertical_alignment,
shaping,
});
let size = limits.resolve(paragraph.min_bounds());

View file

@ -0,0 +1,15 @@
[package]
name = "editor"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"]
edition = "2021"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["highlighter", "tokio", "debug"]
tokio.workspace = true
tokio.features = ["fs"]
rfd = "0.12"

Binary file not shown.

312
examples/editor/src/main.rs Normal file
View file

@ -0,0 +1,312 @@
use iced::executor;
use iced::highlighter::{self, Highlighter};
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,
Subscription,
};
use std::ffi;
use std::io;
use std::path::{Path, PathBuf};
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::MONOSPACE,
..Settings::default()
})
}
struct Editor {
file: Option<PathBuf>,
content: text_editor::Content,
theme: highlighter::Theme,
is_loading: bool,
is_dirty: bool,
}
#[derive(Debug, Clone)]
enum Message {
ActionPerformed(text_editor::Action),
ThemeSelected(highlighter::Theme),
NewFile,
OpenFile,
FileOpened(Result<(PathBuf, Arc<String>), Error>),
SaveFile,
FileSaved(Result<PathBuf, Error>),
}
impl Application for Editor {
type Message = Message;
type Theme = Theme;
type Executor = executor::Default;
type Flags = ();
fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
(
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) -> Command<Message> {
match message {
Message::ActionPerformed(action) => {
self.is_dirty = self.is_dirty || action.is_edit();
self.content.perform(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_text(&contents);
}
Command::none()
}
Message::SaveFile => {
if self.is_loading {
Command::none()
} else {
self.is_loading = true;
Command::perform(
save_file(self.file.clone(), self.content.text()),
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 subscription(&self) -> Subscription<Message> {
keyboard::on_key_press(|key_code, modifiers| match key_code {
keyboard::KeyCode::S if modifiers.command() => {
Some(Message::SaveFile)
}
_ => None,
})
}
fn view(&self) -> Element<Message> {
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)
.align_items(Alignment::Center);
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![
controls,
text_editor(&self.content)
.on_action(Message::ActionPerformed)
.highlight::<Highlighter>(
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)
.padding(10)
.into()
}
fn theme(&self) -> Theme {
if self.theme.is_dark() {
Theme::Dark
} else {
Theme::Light
}
}
}
#[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<String>), 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<String>), 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<PathBuf>,
contents: String,
) -> Result<PathBuf, Error> {
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)?
};
tokio::fs::write(&path, contents)
.await
.map_err(|error| Error::IoError(error.kind()))?;
Ok(path)
}
fn action<'a, Message: Clone + 'a>(
content: impl Into<Element<'a, Message>>,
label: &'a str,
on_press: Option<Message>,
) -> Element<'a, Message> {
let action = button(container(content).width(30).center_x());
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()
}

View file

@ -25,16 +25,16 @@ 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
unicode-segmentation.workspace = true
image.workspace = true
image.optional = true
@ -42,7 +42,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

View file

@ -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.

View file

@ -66,6 +66,13 @@ impl<T: Damage> Damage for Primitive<T> {
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),

View file

@ -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

View file

@ -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<T> {
/// 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

View file

@ -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}';
@ -158,41 +159,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,
@ -206,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>,

View file

@ -1,68 +1,74 @@
//! Draw text.
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;
use crate::color;
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;
use std::sync::{self, Arc, RwLock};
use std::sync::{Arc, RwLock};
/// Returns the global [`FontSystem`].
pub fn font_system() -> &'static RwLock<FontSystem> {
static FONT_SYSTEM: OnceCell<RwLock<FontSystem>> = OnceCell::new();
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(),
)),
]),
version: Version::default(),
})
})
}
/// A set of system fonts.
#[allow(missing_debug_implementations)]
pub struct FontSystem {
raw: RwLock<cosmic_text::FontSystem>,
raw: cosmic_text::FontSystem,
version: Version,
}
impl FontSystem {
pub fn new() -> Self {
FontSystem {
raw: RwLock::new(cosmic_text::FontSystem::new_with_fonts([
cosmic_text::fontdb::Source::Binary(Arc::new(
include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
)),
])),
version: Version::default(),
}
}
pub fn get_mut(&mut self) -> &mut cosmic_text::FontSystem {
self.raw.get_mut().expect("Lock font system")
}
pub fn write(
&self,
) -> (sync::RwLockWriteGuard<'_, cosmic_text::FontSystem>, Version) {
(self.raw.write().expect("Write font system"), self.version)
/// 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.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())),
);
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);
impl Default for FontSystem {
fn default() -> Self {
Self::new()
}
}
/// Measures the dimensions of the given [`cosmic_text::Buffer`].
pub fn measure(buffer: &cosmic_text::Buffer) -> Size {
let (width, total_lines) = buffer
.layout_runs()
@ -73,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))
@ -128,9 +135,22 @@ 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,
Shaping::Advanced => cosmic_text::Shaping::Advanced,
}
}
/// 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();
cosmic_text::Color::rgba(
(r * 255.0) as u8,
(g * 255.0) as u8,
(b * 255.0) as u8,
(a * 255.0) as u8,
)
}

View file

@ -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<twox_hash::XxHash64>;
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,
}

779
graphics/src/text/editor.rs Normal file
View file

@ -0,0 +1,779 @@
//! Draw and edit text.
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 as _;
use std::fmt;
use std::sync::{self, Arc};
/// A multi-line text editor.
#[derive(Debug, PartialEq)]
pub struct Editor(Option<Arc<Internal>>);
struct Internal {
editor: cosmic_text::Editor,
font: Font,
bounds: Size,
topmost_line_changed: Option<usize>,
version: text::Version,
}
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();
Weak {
raw: Arc::downgrade(editor),
bounds: editor.bounds,
}
}
fn internal(&self) -> &Arc<Internal> {
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,
});
let mut font_system =
text::font_system().write().expect("Write font system");
buffer.set_text(
font_system.raw(),
text,
cosmic_text::Attrs::new(),
cosmic_text::Shaping::Advanced,
);
Editor(Some(Arc::new(Internal {
editor: cosmic_text::Editor::new(buffer),
version: font_system.version(),
..Default::default()
})))
}
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<String> {
self.internal().editor.copy_selection()
}
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) => {
let (start, end) = if cursor < selection {
(cursor, selection)
} else {
(selection, cursor)
};
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()
.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 i32 + visual_lines_offset)
as f32
* line_height,
height: line_height,
})
} else {
None
}
})
.collect();
Cursor::Selection(regions)
}
_ => {
let line_height = buffer.metrics().line_height;
let visual_lines_offset =
visual_lines_offset(cursor.line, buffer);
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 (visual_line, 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_before_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_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()
.take_while(|glyph| cursor.index > glyph.start)
.map(|glyph| glyph.w)
.sum();
Some((i, offset))
} else {
None
}
})
.unwrap_or((
layout.len().saturating_sub(1),
layout.last().map(|line| line.w).unwrap_or(0.0),
));
Cursor::Caret(Point::new(
offset,
(visual_lines_offset + visual_line as i32) as f32
* line_height,
))
}
}
}
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");
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;
match action {
// Motion events
Action::Move(motion) => {
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(), motion_to_action(motion));
}
}
// Selection events
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 => {
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 => {
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::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
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);
}
}
}
Action::Scroll { lines } => {
editor.action(
font_system.raw(),
cosmic_text::Action::Scroll { lines },
);
}
}
self.0 = Some(Arc::new(internal));
}
fn bounds(&self) -> Size {
self.internal().bounds
}
fn update(
&mut self,
new_bounds: Size,
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");
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");
if font_system.version() != internal.version {
log::trace!("Updating `FontSystem` of `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 {
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);
}
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
{
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),
);
}
if new_bounds != internal.bounds {
log::trace!("Updating size of `Editor`...");
internal.editor.buffer_mut().set_size(
font_system.raw(),
new_bounds.width,
new_bounds.height,
);
internal.bounds = new_bounds;
}
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);
}
internal.editor.shape_as_needed(font_system.raw());
self.0 = Some(Arc::new(internal));
}
fn highlight<H: Highlighter>(
&mut self,
font: Self::Font,
highlighter: &mut H,
format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>,
) {
let internal = self.internal();
let buffer = internal.editor.buffer();
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().saturating_sub(1));
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);
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);
}
internal.editor.shape_as_needed(font_system.raw());
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.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,
topmost_line_changed: None,
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)
.finish()
}
}
/// A weak reference to an [`Editor`].
#[derive(Debug, Clone)]
pub struct Weak {
raw: sync::Weak<Internal>,
/// The bounds of the [`Editor`].
pub bounds: Size,
}
impl Weak {
/// Tries to update the reference into an [`Editor`].
pub fn upgrade(&self) -> Option<Editor> {
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,
}
}
}
fn highlight_line(
line: &cosmic_text::BufferLine,
from: usize,
to: usize,
) -> impl Iterator<Item = (f32, f32)> + '_ {
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)
}
})
}
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()
}
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,
}
}

View file

@ -1,12 +1,14 @@
//! Draw paragraphs.
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};
/// A bunch of text.
#[derive(Clone, PartialEq)]
pub struct Paragraph(Option<Arc<Internal>>);
@ -23,17 +25,50 @@ struct Internal {
}
impl Paragraph {
/// Creates a new empty [`Paragraph`].
pub fn new() -> Self {
Self::default()
}
pub fn with_text(text: Text<'_, Font>, font_system: &FontSystem) -> Self {
/// 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 [`Paragraph`] when
/// referential guarantees are unnecessary. For instance,
/// when creating a rendering tree.
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<Internal> {
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 +76,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 +99,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 +111,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 +130,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<Internal> {
self.0
.as_ref()
.expect("paragraph should always be initialized")
}
}
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();
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
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 +176,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
}
@ -278,15 +278,20 @@ impl Default for Internal {
}
}
/// A weak reference to a [`Paragraph`].
#[derive(Debug, Clone)]
pub struct Weak {
raw: sync::Weak<Internal>,
/// 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<Paragraph> {
self.raw.upgrade().map(Some).map(Paragraph)
}

17
highlighter/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "iced_highlighter"
description = "A syntax highlighter 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
once_cell.workspace = true
syntect.workspace = true

245
highlighter/src/lib.rs Normal file
View file

@ -0,0 +1,245 @@
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<parsing::SyntaxSet> =
Lazy::new(parsing::SyntaxSet::load_defaults_nonewlines);
static THEMES: Lazy<highlighting::ThemeSet> =
Lazy::new(highlighting::ThemeSet::load_defaults);
const LINES_PER_SNAPSHOT: usize = 50;
pub struct Highlighter {
syntax: &'static parsing::SyntaxReference,
highlighter: highlighting::Highlighter<'static>,
caches: Vec<(parsing::ParseState, parsing::ScopeStack)>,
current_line: usize,
}
impl highlighter::Highlighter for Highlighter {
type Settings = Settings;
type Highlight = Highlight;
type Iterator<'a> =
Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>;
fn new(settings: &Self::Settings) -> Self {
let syntax = SYNTAXES
.find_syntax_by_token(&settings.extension)
.unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
let highlighter = highlighting::Highlighter::new(
&THEMES.themes[settings.theme.key()],
);
let parser = parsing::ParseState::new(syntax);
let stack = parsing::ScopeStack::new();
Highlighter {
syntax,
highlighter,
caches: vec![(parser, stack)],
current_line: 0,
}
}
fn update(&mut self, new_settings: &Self::Settings) {
self.syntax = SYNTAXES
.find_syntax_by_token(&new_settings.extension)
.unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
self.highlighter = highlighting::Highlighter::new(
&THEMES.themes[new_settings.theme.key()],
);
// 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, &SYNTAXES).unwrap_or_default();
let highlighter = &self.highlighter;
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<Color> {
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<Font> {
None
}
pub fn to_format(&self) -> Format<Font> {
Format {
color: self.color(),
font: self.font(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Theme {
SolarizedDark,
Base16Mocha,
Base16Ocean,
Base16Eighties,
InspiredGitHub,
}
impl Theme {
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::SolarizedDark => "Solarized (dark)",
Theme::Base16Mocha => "base16-mocha.dark",
Theme::Base16Ocean => "base16-ocean.dark",
Theme::Base16Eighties => "base16-eighties.dark",
Theme::InspiredGitHub => "InspiredGitHub",
}
}
}
impl std::fmt::Display for Theme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
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"),
}
}
}
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<usize>, parsing::ScopeStackOp);
fn next(&mut self) -> Option<Self::Item> {
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))
}
}

View file

@ -19,9 +19,8 @@ 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;
@ -149,6 +148,7 @@ impl<T> core::Renderer for Renderer<T> {
impl<T> text::Renderer for Renderer<T> {
type Font = Font;
type Paragraph = Paragraph;
type Editor = Editor;
const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;
const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON;
@ -163,36 +163,33 @@ impl<T> text::Renderer for Renderer<T> {
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));
}
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)
);
}
@ -210,7 +207,10 @@ impl<T> text::Renderer for Renderer<T> {
impl<T> crate::core::image::Renderer for Renderer<T> {
type Handle = crate::core::image::Handle;
fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
fn dimensions(
&self,
handle: &crate::core::image::Handle,
) -> core::Size<u32> {
delegate!(self, renderer, renderer.dimensions(handle))
}
@ -221,7 +221,7 @@ impl<T> crate::core::image::Renderer for Renderer<T> {
#[cfg(feature = "svg")]
impl<T> crate::core::svg::Renderer for Renderer<T> {
fn dimensions(&self, handle: &crate::core::svg::Handle) -> Size<u32> {
fn dimensions(&self, handle: &crate::core::svg::Handle) -> core::Size<u32> {
delegate!(self, renderer, renderer.dimensions(handle))
}

View file

@ -168,6 +168,9 @@ use iced_winit::runtime;
pub use iced_futures::futures;
#[cfg(feature = "highlighter")]
pub use iced_highlighter as highlighter;
mod error;
mod sandbox;

View file

@ -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<Flags> {
@ -21,6 +23,9 @@ pub struct Settings<Flags> {
/// [`Application`]: crate::Application
pub flags: Flags,
/// The fonts to load on boot.
pub fonts: Vec<Cow<'static, [u8]>>,
/// The default [`Font`] to be used.
///
/// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif).
@ -62,6 +67,7 @@ impl<Flags> Settings<Flags> {
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: window::Settings::default(),
flags: Default::default(),
fonts: Vec::new(),
default_font: Font::default(),
default_text_size: Pixels(16.0),
antialiasing: false,
@ -93,6 +100,7 @@ impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> {
id: settings.id,
window: settings.window.into(),
flags: settings.flags,
fonts: settings.fonts,
exit_on_close_request: settings.exit_on_close_request,
}
}

View file

@ -29,6 +29,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;

47
style/src/text_editor.rs Normal file
View file

@ -0,0 +1,47 @@
//! Change the appearance of a text editor.
use crate::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;
}

View file

@ -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<dyn text_editor::StyleSheet<Style = Theme>>),
}
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)
}
}

View file

@ -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};
@ -384,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,
@ -803,10 +827,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);
}

View file

@ -1,9 +1,11 @@
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;
use crate::graphics::text::paragraph;
use crate::graphics::text::FontSystem;
use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow;
@ -12,7 +14,6 @@ use std::collections::hash_map;
#[allow(missing_debug_implementations)]
pub struct Pipeline {
font_system: FontSystem,
glyph_cache: GlyphCache,
cache: RefCell<Cache>,
}
@ -20,18 +21,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 +50,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()),
@ -65,6 +66,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.bounds()),
color,
alignment::Horizontal::Left,
alignment::Vertical::Top,
scale_factor,
pixels,
clip_mask,
);
}
pub fn draw_cached(
&mut self,
content: &str,
@ -82,7 +114,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,
@ -155,7 +189,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,
) {
@ -180,6 +214,23 @@ fn draw(
}
}
fn from_color(color: cosmic_text::Color) -> Color {
let [r, g, b, a] = color.as_rgba();
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)]
struct GlyphCache {
entries: FxHashMap<

View file

@ -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};
@ -314,10 +313,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);
}

View file

@ -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,

View file

@ -1,16 +1,27 @@
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.
/// A text primitive.
#[derive(Debug, Clone)]
pub enum Text<'a> {
Managed {
/// 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>),
}

View file

@ -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

View file

@ -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, to_color, Editor, 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<glyphon::TextRenderer>,
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,21 +66,27 @@ 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();
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,
@ -118,7 +121,7 @@ impl Pipeline {
vertical_alignment,
color,
) = match section {
Text::Managed {
Text::Paragraph {
position, color, ..
} => {
use crate::core::text::Paragraph as _;
@ -136,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.bounds()),
alignment::Horizontal::Left,
alignment::Vertical::Top,
*color,
)
}
Text::Cached(text) => {
let Some(Allocation::Cache(key)) = allocation else {
return None;
@ -193,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),
})
},
);

View file

@ -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<Message, Renderer>(
content: &text_editor::Content<Renderer>,
) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Renderer>
where
Message: Clone,
Renderer: core::text::Renderer,
Renderer::Theme: text_editor::StyleSheet,
{
TextEditor::new(content)
}
/// Creates a new [`Slider`].
///
/// [`Slider`]: crate::Slider

View file

@ -35,6 +35,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;
@ -86,6 +87,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;

View file

@ -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 {

708
widget/src/text_editor.rs Normal file
View file

@ -0,0 +1,708 @@
//! 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};
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::{
Clipboard, Color, Element, Length, Padding, Pixels, Rectangle, Shell,
Vector,
};
use std::cell::RefCell;
use std::fmt;
use std::ops::DerefMut;
use std::sync::Arc;
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,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
content: &'a Content<Renderer>,
font: Option<Renderer::Font>,
text_size: Option<Pixels>,
line_height: LineHeight,
width: Length,
height: Length,
padding: Padding,
style: <Renderer::Theme as StyleSheet>::Style,
on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
highlighter_settings: Highlighter::Settings,
highlighter_format: fn(
&Highlighter::Highlight,
&Renderer::Theme,
) -> highlighter::Format<Renderer::Font>,
}
impl<'a, Message, Renderer>
TextEditor<'a, highlighter::PlainText, Message, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
/// Creates new [`TextEditor`] with the given [`Content`].
pub fn new(content: &'a Content<Renderer>) -> 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,
highlighter_settings: (),
highlighter_format: |_highlight, _theme| {
highlighter::Format::default()
},
}
}
}
impl<'a, Highlighter, Message, Renderer>
TextEditor<'a, Highlighter, Message, Renderer>
where
Highlighter: text::Highlighter,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
/// 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<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Sets the [`Padding`] of the [`TextEditor`].
pub fn padding(mut self, padding: impl Into<Padding>) -> 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<H: text::Highlighter>(
self,
settings: H::Settings,
to_format: fn(
&H::Highlight,
&Renderer::Theme,
) -> highlighter::Format<Renderer::Font>,
) -> 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,
highlighter_format: to_format,
}
}
}
/// The content of a [`TextEditor`].
pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>)
where
R: text::Renderer;
struct Internal<R>
where
R: text::Renderer,
{
editor: R::Editor,
is_dirty: bool,
}
impl<R> Content<R>
where
R: text::Renderer,
{
/// Creates an empty [`Content`].
pub fn new() -> Self {
Self::with_text("")
}
/// Creates a [`Content`] with the given text.
pub fn with_text(text: &str) -> Self {
Self(RefCell::new(Internal {
editor: R::Editor::with_text(text),
is_dirty: true,
}))
}
/// 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,
) -> Option<impl std::ops::Deref<Target = str> + '_> {
std::cell::Ref::filter_map(self.0.borrow(), |internal| {
internal.editor.line(index)
})
.ok()
}
/// Returns an iterator of the text of the lines in the [`Content`].
pub fn lines(
&self,
) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> {
struct Lines<'a, Renderer: text::Renderer> {
internal: std::cell::Ref<'a, Internal<Renderer>>,
current: usize,
}
impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> {
type Item = std::cell::Ref<'a, str>;
fn next(&mut self) -> Option<Self::Item> {
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,
}
}
/// 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(),
|mut contents, (i, line)| {
if i > 0 {
contents.push('\n');
}
contents.push_str(&line);
contents
},
);
if !text.ends_with('\n') {
text.push('\n');
}
text
}
/// Returns the selected text of the [`Content`].
pub fn selection(&self) -> Option<String> {
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()
}
}
impl<Renderer> Default for Content<Renderer>
where
Renderer: text::Renderer,
{
fn default() -> Self {
Self::new()
}
}
impl<Renderer> fmt::Debug for Content<Renderer>
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<Highlighter: text::Highlighter> {
is_focused: bool,
last_click: Option<mouse::Click>,
drag_click: Option<mouse::click::Kind>,
highlighter: RefCell<Highlighter>,
highlighter_settings: Highlighter::Settings,
highlighter_format_address: usize,
}
impl<'a, Highlighter, Message, Renderer> Widget<Message, Renderer>
for TextEditor<'a, Highlighter, Message, Renderer>
where
Highlighter: text::Highlighter,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> widget::tree::Tag {
widget::tree::Tag::of::<State<Highlighter>>()
}
fn state(&self) -> widget::tree::State {
widget::tree::State::new(State {
is_focused: false,
last_click: None,
drag_click: None,
highlighter: RefCell::new(Highlighter::new(
&self.highlighter_settings,
)),
highlighter_settings: self.highlighter_settings.clone(),
highlighter_format_address: self.highlighter_format as usize,
})
}
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();
let state = tree.state.downcast_mut::<State<Highlighter>>();
if state.highlighter_format_address != self.highlighter_format as usize
{
state.highlighter.borrow_mut().change_line(0);
state.highlighter_format_address = self.highlighter_format as usize;
}
if state.highlighter_settings != self.highlighter_settings {
state
.highlighter
.borrow_mut()
.update(&self.highlighter_settings);
state.highlighter_settings = self.highlighter_settings.clone();
}
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())
}
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::<State<Highlighter>>();
let Some(update) = Update::from_event(
event,
state,
layout.bounds(),
self.padding,
cursor,
) else {
return event::Status::Ignored;
};
match update {
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.last_click = Some(click);
state.drag_click = Some(click.kind());
shell.publish(on_edit(action));
}
Update::Unfocus => {
state.is_focused = false;
state.drag_click = None;
}
Update::Release => {
state.drag_click = None;
}
Update::Action(action) => {
shell.publish(on_edit(action));
}
Update::Copy => {
if let Some(selection) = self.content.selection() {
clipboard.write(selection);
}
}
Update::Paste => {
if let Some(contents) = clipboard.read() {
shell.publish(on_edit(Action::Edit(Edit::Paste(
Arc::new(contents),
))));
}
}
}
event::Status::Captured
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
theme: &<Renderer as renderer::Renderer>::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let mut internal = self.content.0.borrow_mut();
let state = tree.state.downcast_ref::<State<Highlighter>>();
internal.editor.highlight(
self.font.unwrap_or_else(|| renderer.default_font()),
state.highlighter.borrow_mut().deref_mut(),
|highlight| (self.highlighter_format)(highlight, theme),
);
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,
);
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) => {
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,
},
theme.value_color(&self.style),
);
}
}
Cursor::Selection(ranges) => {
for range in ranges.into_iter().filter_map(|range| {
bounds.intersection(&(range + translation))
}) {
renderer.fill_quad(
renderer::Quad {
bounds: range,
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, Highlighter, Message, Renderer>
From<TextEditor<'a, Highlighter, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Highlighter: text::Highlighter,
Message: 'a,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(
text_editor: TextEditor<'a, Highlighter, Message, Renderer>,
) -> Self {
Self::new(text_editor)
}
}
enum Update {
Click(mouse::Click),
Unfocus,
Release,
Action(Action),
Copy,
Paste,
}
impl Update {
fn from_event<H: Highlighter>(
event: Event,
state: &State<H>,
bounds: Rectangle,
padding: Padding,
cursor: mouse::Cursor,
) -> Option<Self> {
let action = |action| Some(Update::Action(action));
let edit = |edit| action(Action::Edit(edit));
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);
let click = mouse::Click::new(
cursor_position,
state.last_click,
);
Some(Update::Click(click))
} else if state.is_focused {
Some(Update::Unfocus)
} else {
None
}
}
mouse::Event::ButtonReleased(mouse::Button::Left) => {
Some(Update::Release)
}
mouse::Event::CursorMoved { .. } => match state.drag_click {
Some(mouse::click::Kind::Single) => {
let cursor_position = cursor.position_in(bounds)?
- Vector::new(padding.top, padding.left);
action(Action::Drag(cursor_position))
}
_ => None,
},
mouse::Event::WheelScrolled { delta }
if cursor.is_over(bounds) =>
{
action(Action::Scroll {
lines: match delta {
mouse::ScrollDelta::Lines { y, .. } => {
if y.abs() > 0.0 {
(y.signum() * -(y.abs() * 4.0).max(1.0))
as i32
} else {
0
}
}
mouse::ScrollDelta::Pixels { y, .. } => {
(-y / 4.0) as i32
}
},
})
}
_ => None,
},
Event::Keyboard(event) => match event {
keyboard::Event::KeyPressed {
key_code,
modifiers,
} if state.is_focused => {
if let Some(motion) = motion(key_code) {
let motion =
if platform::is_jump_modifier_pressed(modifiers) {
motion.widen()
} else {
motion
};
return action(if modifiers.shift() {
Action::Select(motion)
} else {
Action::Move(motion)
});
}
match key_code {
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)
}
keyboard::KeyCode::V
if modifiers.command() && !modifiers.alt() =>
{
Some(Self::Paste)
}
_ => None,
}
}
keyboard::Event::CharacterReceived(c) if state.is_focused => {
edit(Edit::Insert(c))
}
_ => None,
},
_ => None,
}
}
}
fn motion(key_code: keyboard::KeyCode) -> Option<Motion> {
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;
pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
if cfg!(target_os = "macos") {
modifiers.alt()
} else {
modifiers.control()
}
}
}

View file

@ -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<Renderer>(
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(),

View file

@ -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();

View file

@ -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<Flags> {
/// [`Application`]: crate::Application
pub flags: Flags,
/// The fonts to load on boot.
pub fonts: Vec<Cow<'static, [u8]>>,
/// Whether the [`Application`] should exit when the user requests the
/// window to close (e.g. the user presses the close button).
///