Merge pull request #2123 from iced-rs/text-editor
`TextEditor` widget (or multi-line text input)
This commit is contained in:
commit
d731996342
50 changed files with 3142 additions and 424 deletions
|
|
@ -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 \
|
||||
|
|
|
|||
1
.github/workflows/document.yml
vendored
1
.github/workflows/document.yml
vendored
|
|
@ -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 \
|
||||
|
|
|
|||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
10
Cargo.toml
10
Cargo.toml
|
|
@ -47,6 +47,8 @@ system = ["iced_winit/system"]
|
|||
web-colors = ["iced_renderer/web-colors"]
|
||||
# Enables the WebGL backend, replacing WebGPU
|
||||
webgl = ["iced_renderer/webgl"]
|
||||
# Enables the syntax `highlighter` module
|
||||
highlighter = ["iced_highlighter"]
|
||||
# Enables the advanced module
|
||||
advanced = []
|
||||
|
||||
|
|
@ -58,6 +60,9 @@ iced_widget.workspace = true
|
|||
iced_winit.features = ["application"]
|
||||
iced_winit.workspace = true
|
||||
|
||||
iced_highlighter.workspace = true
|
||||
iced_highlighter.optional = true
|
||||
|
||||
thiserror.workspace = true
|
||||
|
||||
image.workspace = true
|
||||
|
|
@ -78,8 +83,9 @@ members = [
|
|||
"core",
|
||||
"futures",
|
||||
"graphics",
|
||||
"runtime",
|
||||
"highlighter",
|
||||
"renderer",
|
||||
"runtime",
|
||||
"style",
|
||||
"tiny_skia",
|
||||
"wgpu",
|
||||
|
|
@ -103,6 +109,7 @@ iced = { version = "0.12", path = "." }
|
|||
iced_core = { version = "0.12", path = "core" }
|
||||
iced_futures = { version = "0.12", path = "futures" }
|
||||
iced_graphics = { version = "0.12", path = "graphics" }
|
||||
iced_highlighter = { version = "0.12", path = "highlighter" }
|
||||
iced_renderer = { version = "0.12", path = "renderer" }
|
||||
iced_runtime = { version = "0.12", path = "runtime" }
|
||||
iced_style = { version = "0.12", path = "style" }
|
||||
|
|
@ -137,6 +144,7 @@ resvg = "0.35"
|
|||
rustc-hash = "1.0"
|
||||
smol = "1.0"
|
||||
softbuffer = "0.2"
|
||||
syntect = "5.1"
|
||||
sysinfo = "0.28"
|
||||
thiserror = "1.0"
|
||||
tiny-skia = "0.10"
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
173
core/src/text.rs
173
core/src/text.rs
|
|
@ -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
181
core/src/text/editor.rs
Normal 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>),
|
||||
}
|
||||
88
core/src/text/highlighter.rs
Normal file
88
core/src/text/highlighter.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
59
core/src/text/paragraph.rs
Normal file
59
core/src/text/paragraph.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
15
examples/editor/Cargo.toml
Normal file
15
examples/editor/Cargo.toml
Normal 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"
|
||||
BIN
examples/editor/fonts/icons.ttf
Normal file
BIN
examples/editor/fonts/icons.ttf
Normal file
Binary file not shown.
312
examples/editor/src/main.rs
Normal file
312
examples/editor/src/main.rs
Normal 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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
779
graphics/src/text/editor.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
17
highlighter/Cargo.toml
Normal 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
245
highlighter/src/lib.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
47
style/src/text_editor.rs
Normal 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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
708
widget/src/text_editor.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
///
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue