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
|
-D clippy::useless_conversion
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
|
|
||||||
|
|
||||||
nitpick = """
|
nitpick = """
|
||||||
clippy --workspace --no-deps -- \
|
clippy --workspace --no-deps -- \
|
||||||
-D warnings \
|
-D warnings \
|
||||||
|
|
|
||||||
1
.github/workflows/document.yml
vendored
1
.github/workflows/document.yml
vendored
|
|
@ -15,6 +15,7 @@ jobs:
|
||||||
RUSTDOCFLAGS="--cfg docsrs" \
|
RUSTDOCFLAGS="--cfg docsrs" \
|
||||||
cargo doc --no-deps --all-features \
|
cargo doc --no-deps --all-features \
|
||||||
-p iced_core \
|
-p iced_core \
|
||||||
|
-p iced_highlighter \
|
||||||
-p iced_style \
|
-p iced_style \
|
||||||
-p iced_futures \
|
-p iced_futures \
|
||||||
-p iced_runtime \
|
-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]
|
on: [push, pull_request]
|
||||||
jobs:
|
jobs:
|
||||||
all:
|
all:
|
||||||
runs-on: ubuntu-latest
|
runs-on: macOS-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: hecrj/setup-rust-action@v1
|
- uses: hecrj/setup-rust-action@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
export DEBIAN_FRONTED=noninteractive
|
export DEBIAN_FRONTED=noninteractive
|
||||||
sudo apt-get -qq update
|
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
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
cargo test --verbose --workspace
|
cargo test --verbose --workspace
|
||||||
|
|
|
||||||
10
Cargo.toml
10
Cargo.toml
|
|
@ -47,6 +47,8 @@ system = ["iced_winit/system"]
|
||||||
web-colors = ["iced_renderer/web-colors"]
|
web-colors = ["iced_renderer/web-colors"]
|
||||||
# Enables the WebGL backend, replacing WebGPU
|
# Enables the WebGL backend, replacing WebGPU
|
||||||
webgl = ["iced_renderer/webgl"]
|
webgl = ["iced_renderer/webgl"]
|
||||||
|
# Enables the syntax `highlighter` module
|
||||||
|
highlighter = ["iced_highlighter"]
|
||||||
# Enables the advanced module
|
# Enables the advanced module
|
||||||
advanced = []
|
advanced = []
|
||||||
|
|
||||||
|
|
@ -58,6 +60,9 @@ iced_widget.workspace = true
|
||||||
iced_winit.features = ["application"]
|
iced_winit.features = ["application"]
|
||||||
iced_winit.workspace = true
|
iced_winit.workspace = true
|
||||||
|
|
||||||
|
iced_highlighter.workspace = true
|
||||||
|
iced_highlighter.optional = true
|
||||||
|
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
|
|
@ -78,8 +83,9 @@ members = [
|
||||||
"core",
|
"core",
|
||||||
"futures",
|
"futures",
|
||||||
"graphics",
|
"graphics",
|
||||||
"runtime",
|
"highlighter",
|
||||||
"renderer",
|
"renderer",
|
||||||
|
"runtime",
|
||||||
"style",
|
"style",
|
||||||
"tiny_skia",
|
"tiny_skia",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
|
|
@ -103,6 +109,7 @@ iced = { version = "0.12", path = "." }
|
||||||
iced_core = { version = "0.12", path = "core" }
|
iced_core = { version = "0.12", path = "core" }
|
||||||
iced_futures = { version = "0.12", path = "futures" }
|
iced_futures = { version = "0.12", path = "futures" }
|
||||||
iced_graphics = { version = "0.12", path = "graphics" }
|
iced_graphics = { version = "0.12", path = "graphics" }
|
||||||
|
iced_highlighter = { version = "0.12", path = "highlighter" }
|
||||||
iced_renderer = { version = "0.12", path = "renderer" }
|
iced_renderer = { version = "0.12", path = "renderer" }
|
||||||
iced_runtime = { version = "0.12", path = "runtime" }
|
iced_runtime = { version = "0.12", path = "runtime" }
|
||||||
iced_style = { version = "0.12", path = "style" }
|
iced_style = { version = "0.12", path = "style" }
|
||||||
|
|
@ -137,6 +144,7 @@ resvg = "0.35"
|
||||||
rustc-hash = "1.0"
|
rustc-hash = "1.0"
|
||||||
smol = "1.0"
|
smol = "1.0"
|
||||||
softbuffer = "0.2"
|
softbuffer = "0.2"
|
||||||
|
syntect = "5.1"
|
||||||
sysinfo = "0.28"
|
sysinfo = "0.28"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tiny-skia = "0.10"
|
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.
|
/// Converts the [`Color`] into its RGBA8 equivalent.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_rgba8(self) -> [u8; 4] {
|
pub fn into_rgba8(self) -> [u8; 4] {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ pub struct Font {
|
||||||
pub stretch: Stretch,
|
pub stretch: Stretch,
|
||||||
/// The [`Style`] of the [`Font`].
|
/// The [`Style`] of the [`Font`].
|
||||||
pub style: Style,
|
pub style: Style,
|
||||||
/// Whether if the [`Font`] is monospaced or not.
|
|
||||||
pub monospaced: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Font {
|
impl Font {
|
||||||
|
|
@ -23,13 +21,11 @@ impl Font {
|
||||||
weight: Weight::Normal,
|
weight: Weight::Normal,
|
||||||
stretch: Stretch::Normal,
|
stretch: Stretch::Normal,
|
||||||
style: Style::Normal,
|
style: Style::Normal,
|
||||||
monospaced: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A monospaced font with normal [`Weight`].
|
/// A monospaced font with normal [`Weight`].
|
||||||
pub const MONOSPACE: Font = Font {
|
pub const MONOSPACE: Font = Font {
|
||||||
family: Family::Monospace,
|
family: Family::Monospace,
|
||||||
monospaced: true,
|
|
||||||
..Self::DEFAULT
|
..Self::DEFAULT
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
use crate::{Length, Padding, Size};
|
use crate::{Length, Padding, Size};
|
||||||
|
|
||||||
/// A set of size constraints for layouting.
|
/// A set of size constraints for layouting.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Limits {
|
pub struct Limits {
|
||||||
min: Size,
|
min: Size,
|
||||||
max: Size,
|
max: Size,
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,11 @@ impl Click {
|
||||||
self.kind
|
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 {
|
fn is_consecutive(&self, new_position: Point, time: Instant) -> bool {
|
||||||
let duration = if time > self.time {
|
let duration = if time > self.time {
|
||||||
Some(time - self.time)
|
Some(time - self.time)
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ impl Renderer for Null {
|
||||||
impl text::Renderer for Null {
|
impl text::Renderer for Null {
|
||||||
type Font = Font;
|
type Font = Font;
|
||||||
type Paragraph = ();
|
type Paragraph = ();
|
||||||
|
type Editor = ();
|
||||||
|
|
||||||
const ICON_FONT: Font = Font::DEFAULT;
|
const ICON_FONT: Font = Font::DEFAULT;
|
||||||
const CHECKMARK_ICON: char = '0';
|
const CHECKMARK_ICON: char = '0';
|
||||||
|
|
@ -58,16 +59,6 @@ impl text::Renderer for Null {
|
||||||
|
|
||||||
fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
|
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(
|
fn fill_paragraph(
|
||||||
&mut self,
|
&mut self,
|
||||||
_paragraph: &Self::Paragraph,
|
_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(
|
fn fill_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
_paragraph: Text<'_, Self::Font>,
|
_paragraph: Text<'_, Self::Font>,
|
||||||
|
|
@ -88,24 +87,12 @@ impl text::Renderer for Null {
|
||||||
impl text::Paragraph for () {
|
impl text::Paragraph for () {
|
||||||
type Font = Font;
|
type Font = Font;
|
||||||
|
|
||||||
fn content(&self) -> &str {
|
fn with_text(_text: Text<'_, Self::Font>) -> Self {}
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
fn text_size(&self) -> Pixels {
|
fn resize(&mut self, _new_bounds: Size) {}
|
||||||
Pixels(16.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn font(&self) -> Self::Font {
|
fn compare(&self, _text: Text<'_, Self::Font>) -> text::Difference {
|
||||||
Font::default()
|
text::Difference::None
|
||||||
}
|
|
||||||
|
|
||||||
fn line_height(&self) -> text::LineHeight {
|
|
||||||
text::LineHeight::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shaping(&self) -> text::Shaping {
|
|
||||||
text::Shaping::default()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn horizontal_alignment(&self) -> alignment::Horizontal {
|
fn horizontal_alignment(&self) -> alignment::Horizontal {
|
||||||
|
|
@ -120,10 +107,6 @@ impl text::Paragraph for () {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bounds(&self) -> Size {
|
|
||||||
Size::ZERO
|
|
||||||
}
|
|
||||||
|
|
||||||
fn min_bounds(&self) -> Size {
|
fn min_bounds(&self) -> Size {
|
||||||
Size::ZERO
|
Size::ZERO
|
||||||
}
|
}
|
||||||
|
|
@ -132,3 +115,55 @@ impl text::Paragraph for () {
|
||||||
None
|
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.
|
//! 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::alignment;
|
||||||
use crate::{Color, Pixels, Point, Size};
|
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`].
|
/// A renderer capable of measuring and drawing [`Text`].
|
||||||
pub trait Renderer: crate::Renderer {
|
pub trait Renderer: crate::Renderer {
|
||||||
/// The font type used.
|
/// The font type used.
|
||||||
|
|
@ -134,6 +170,9 @@ pub trait Renderer: crate::Renderer {
|
||||||
/// The [`Paragraph`] of this [`Renderer`].
|
/// The [`Paragraph`] of this [`Renderer`].
|
||||||
type Paragraph: Paragraph<Font = Self::Font> + 'static;
|
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.
|
/// The icon font of the backend.
|
||||||
const ICON_FONT: Self::Font;
|
const ICON_FONT: Self::Font;
|
||||||
|
|
||||||
|
|
@ -156,33 +195,6 @@ pub trait Renderer: crate::Renderer {
|
||||||
/// Loads a [`Self::Font`] from its bytes.
|
/// Loads a [`Self::Font`] from its bytes.
|
||||||
fn load_font(&mut self, font: Cow<'static, [u8]>);
|
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
|
/// Draws the given [`Paragraph`] at the given position and with the given
|
||||||
/// [`Color`].
|
/// [`Color`].
|
||||||
fn fill_paragraph(
|
fn fill_paragraph(
|
||||||
|
|
@ -192,6 +204,15 @@ pub trait Renderer: crate::Renderer {
|
||||||
color: Color,
|
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
|
/// Draws the given [`Text`] at the given position and with the given
|
||||||
/// [`Color`].
|
/// [`Color`].
|
||||||
fn fill_text(
|
fn fill_text(
|
||||||
|
|
@ -201,101 +222,3 @@ pub trait Renderer: crate::Renderer {
|
||||||
color: Color,
|
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;
|
let State(ref mut paragraph) = state;
|
||||||
|
|
||||||
renderer.update_paragraph(
|
paragraph.update(text::Text {
|
||||||
paragraph,
|
content,
|
||||||
text::Text {
|
bounds,
|
||||||
content,
|
size,
|
||||||
bounds,
|
line_height,
|
||||||
size,
|
font,
|
||||||
line_height,
|
horizontal_alignment,
|
||||||
font,
|
vertical_alignment,
|
||||||
horizontal_alignment,
|
shaping,
|
||||||
vertical_alignment,
|
});
|
||||||
shaping,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let size = limits.resolve(paragraph.min_bounds());
|
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
|
bitflags.workspace = true
|
||||||
bytemuck.workspace = true
|
bytemuck.workspace = true
|
||||||
|
cosmic-text.workspace = true
|
||||||
glam.workspace = true
|
glam.workspace = true
|
||||||
half.workspace = true
|
half.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
once_cell.workspace = true
|
||||||
raw-window-handle.workspace = true
|
raw-window-handle.workspace = true
|
||||||
thiserror.workspace = true
|
|
||||||
cosmic-text.workspace = true
|
|
||||||
rustc-hash.workspace = true
|
rustc-hash.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
lyon_path.workspace = true
|
twox-hash.workspace = true
|
||||||
lyon_path.optional = true
|
unicode-segmentation.workspace = true
|
||||||
|
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
image.optional = true
|
image.optional = true
|
||||||
|
|
@ -42,7 +42,8 @@ image.optional = true
|
||||||
kamadak-exif.workspace = true
|
kamadak-exif.workspace = true
|
||||||
kamadak-exif.optional = true
|
kamadak-exif.optional = true
|
||||||
|
|
||||||
twox-hash.workspace = true
|
lyon_path.workspace = true
|
||||||
|
lyon_path.optional = true
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
twox-hash.workspace = true
|
twox-hash.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
use crate::core::image;
|
use crate::core::image;
|
||||||
use crate::core::svg;
|
use crate::core::svg;
|
||||||
use crate::core::Size;
|
use crate::core::Size;
|
||||||
use crate::text;
|
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
|
@ -18,9 +17,6 @@ pub trait Backend {
|
||||||
pub trait Text {
|
pub trait Text {
|
||||||
/// Loads a font from its bytes.
|
/// Loads a font from its bytes.
|
||||||
fn load_font(&mut self, font: Cow<'static, [u8]>);
|
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.
|
/// A graphics backend that supports image rendering.
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,13 @@ impl<T: Damage> Damage for Primitive<T> {
|
||||||
|
|
||||||
bounds.expand(1.5)
|
bounds.expand(1.5)
|
||||||
}
|
}
|
||||||
|
Self::Editor {
|
||||||
|
editor, position, ..
|
||||||
|
} => {
|
||||||
|
let bounds = Rectangle::new(*position, editor.bounds);
|
||||||
|
|
||||||
|
bounds.expand(1.5)
|
||||||
|
}
|
||||||
Self::Quad { bounds, .. }
|
Self::Quad { bounds, .. }
|
||||||
| Self::Image { bounds, .. }
|
| Self::Image { bounds, .. }
|
||||||
| Self::Svg { bounds, .. } => bounds.expand(1.0),
|
| Self::Svg { bounds, .. } => bounds.expand(1.0),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
#![forbid(rust_2018_idioms)]
|
#![forbid(rust_2018_idioms)]
|
||||||
#![deny(
|
#![deny(
|
||||||
missing_debug_implementations,
|
missing_debug_implementations,
|
||||||
//missing_docs,
|
missing_docs,
|
||||||
unsafe_code,
|
unsafe_code,
|
||||||
unused_results,
|
unused_results,
|
||||||
rustdoc::broken_intra_doc_links
|
rustdoc::broken_intra_doc_links
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use crate::core::image;
|
||||||
use crate::core::svg;
|
use crate::core::svg;
|
||||||
use crate::core::text;
|
use crate::core::text;
|
||||||
use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
|
use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
|
||||||
|
use crate::text::editor;
|
||||||
use crate::text::paragraph;
|
use crate::text::paragraph;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -41,6 +42,15 @@ pub enum Primitive<T> {
|
||||||
/// The color of the paragraph.
|
/// The color of the paragraph.
|
||||||
color: Color,
|
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
|
/// A quad primitive
|
||||||
Quad {
|
Quad {
|
||||||
/// The bounds of the quad
|
/// The bounds of the quad
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ where
|
||||||
{
|
{
|
||||||
type Font = Font;
|
type Font = Font;
|
||||||
type Paragraph = text::Paragraph;
|
type Paragraph = text::Paragraph;
|
||||||
|
type Editor = text::Editor;
|
||||||
|
|
||||||
const ICON_FONT: Font = Font::with_name("Iced-Icons");
|
const ICON_FONT: Font = Font::with_name("Iced-Icons");
|
||||||
const CHECKMARK_ICON: char = '\u{f00c}';
|
const CHECKMARK_ICON: char = '\u{f00c}';
|
||||||
|
|
@ -158,41 +159,6 @@ where
|
||||||
self.backend.load_font(bytes);
|
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(
|
fn fill_paragraph(
|
||||||
&mut self,
|
&mut self,
|
||||||
paragraph: &Self::Paragraph,
|
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(
|
fn fill_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
text: Text<'_, Self::Font>,
|
text: Text<'_, Self::Font>,
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,74 @@
|
||||||
|
//! Draw text.
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
pub mod editor;
|
||||||
pub mod paragraph;
|
pub mod paragraph;
|
||||||
|
|
||||||
pub use cache::Cache;
|
pub use cache::Cache;
|
||||||
|
pub use editor::Editor;
|
||||||
pub use paragraph::Paragraph;
|
pub use paragraph::Paragraph;
|
||||||
|
|
||||||
pub use cosmic_text;
|
pub use cosmic_text;
|
||||||
|
|
||||||
|
use crate::color;
|
||||||
use crate::core::font::{self, Font};
|
use crate::core::font::{self, Font};
|
||||||
use crate::core::text::Shaping;
|
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::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)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct FontSystem {
|
pub struct FontSystem {
|
||||||
raw: RwLock<cosmic_text::FontSystem>,
|
raw: cosmic_text::FontSystem,
|
||||||
version: Version,
|
version: Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FontSystem {
|
impl FontSystem {
|
||||||
pub fn new() -> Self {
|
/// Returns the raw [`cosmic_text::FontSystem`].
|
||||||
FontSystem {
|
pub fn raw(&mut self) -> &mut cosmic_text::FontSystem {
|
||||||
raw: RwLock::new(cosmic_text::FontSystem::new_with_fonts([
|
&mut self.raw
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads a font from its bytes.
|
||||||
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
|
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())),
|
cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.version = Version(self.version.0 + 1);
|
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 {
|
pub fn version(&self) -> Version {
|
||||||
self.version
|
self.version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A version number.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
pub struct Version(u32);
|
pub struct Version(u32);
|
||||||
|
|
||||||
impl Default for FontSystem {
|
/// Measures the dimensions of the given [`cosmic_text::Buffer`].
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn measure(buffer: &cosmic_text::Buffer) -> Size {
|
pub fn measure(buffer: &cosmic_text::Buffer) -> Size {
|
||||||
let (width, total_lines) = buffer
|
let (width, total_lines) = buffer
|
||||||
.layout_runs()
|
.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)
|
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> {
|
pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> {
|
||||||
cosmic_text::Attrs::new()
|
cosmic_text::Attrs::new()
|
||||||
.family(to_family(font.family))
|
.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 {
|
pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
|
||||||
match shaping {
|
match shaping {
|
||||||
Shaping::Basic => cosmic_text::Shaping::Basic,
|
Shaping::Basic => cosmic_text::Shaping::Basic,
|
||||||
Shaping::Advanced => cosmic_text::Shaping::Advanced,
|
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::core::{Font, Size};
|
||||||
use crate::text;
|
use crate::text;
|
||||||
|
|
||||||
|
|
@ -5,6 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use std::collections::hash_map;
|
use std::collections::hash_map;
|
||||||
use std::hash::{BuildHasher, Hash, Hasher};
|
use std::hash::{BuildHasher, Hash, Hasher};
|
||||||
|
|
||||||
|
/// A store of recently used sections of text.
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Cache {
|
pub struct Cache {
|
||||||
|
|
@ -21,14 +23,17 @@ type HashBuilder = twox_hash::RandomXxHashBuilder64;
|
||||||
type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
|
type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
|
||||||
|
|
||||||
impl Cache {
|
impl Cache {
|
||||||
|
/// Creates a new empty [`Cache`].
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the text [`Entry`] with the given [`KeyHash`].
|
||||||
pub fn get(&self, key: &KeyHash) -> Option<&Entry> {
|
pub fn get(&self, key: &KeyHash) -> Option<&Entry> {
|
||||||
self.entries.get(key)
|
self.entries.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allocates a text [`Entry`] if it is not already present in the [`Cache`].
|
||||||
pub fn allocate(
|
pub fn allocate(
|
||||||
&mut self,
|
&mut self,
|
||||||
font_system: &mut cosmic_text::FontSystem,
|
font_system: &mut cosmic_text::FontSystem,
|
||||||
|
|
@ -88,6 +93,9 @@ impl Cache {
|
||||||
(hash, self.entries.get_mut(&hash).unwrap())
|
(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) {
|
pub fn trim(&mut self) {
|
||||||
self.entries
|
self.entries
|
||||||
.retain(|key, _| self.recently_used.contains(key));
|
.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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct Key<'a> {
|
pub struct Key<'a> {
|
||||||
|
/// The content of the text.
|
||||||
pub content: &'a str,
|
pub content: &'a str,
|
||||||
|
/// The size of the text.
|
||||||
pub size: f32,
|
pub size: f32,
|
||||||
|
/// The line height of the text.
|
||||||
pub line_height: f32,
|
pub line_height: f32,
|
||||||
|
/// The [`Font`] of the text.
|
||||||
pub font: Font,
|
pub font: Font,
|
||||||
|
/// The bounds of the text.
|
||||||
pub bounds: Size,
|
pub bounds: Size,
|
||||||
|
/// The shaping strategy of the text.
|
||||||
pub shaping: text::Shaping,
|
pub shaping: text::Shaping,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,10 +138,14 @@ impl Key<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The hash of a [`Key`].
|
||||||
pub type KeyHash = u64;
|
pub type KeyHash = u64;
|
||||||
|
|
||||||
|
/// A cache entry.
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
|
/// The buffer of text, ready for drawing.
|
||||||
pub buffer: cosmic_text::Buffer,
|
pub buffer: cosmic_text::Buffer,
|
||||||
|
/// The minimum bounds of the text.
|
||||||
pub min_bounds: Size,
|
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;
|
||||||
use crate::core::alignment;
|
use crate::core::alignment;
|
||||||
use crate::core::text::{Hit, LineHeight, Shaping, Text};
|
use crate::core::text::{Hit, LineHeight, Shaping, Text};
|
||||||
use crate::core::{Font, Pixels, Point, Size};
|
use crate::core::{Font, Pixels, Point, Size};
|
||||||
use crate::text::{self, FontSystem};
|
use crate::text;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::{self, Arc};
|
use std::sync::{self, Arc};
|
||||||
|
|
||||||
|
/// A bunch of text.
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct Paragraph(Option<Arc<Internal>>);
|
pub struct Paragraph(Option<Arc<Internal>>);
|
||||||
|
|
||||||
|
|
@ -23,17 +25,50 @@ struct Internal {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Paragraph {
|
impl Paragraph {
|
||||||
|
/// Creates a new empty [`Paragraph`].
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
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);
|
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(
|
let mut buffer = cosmic_text::Buffer::new(
|
||||||
&mut font_system,
|
font_system.raw(),
|
||||||
cosmic_text::Metrics::new(
|
cosmic_text::Metrics::new(
|
||||||
text.size.into(),
|
text.size.into(),
|
||||||
text.line_height.to_absolute(text.size).into(),
|
text.line_height.to_absolute(text.size).into(),
|
||||||
|
|
@ -41,13 +76,13 @@ impl Paragraph {
|
||||||
);
|
);
|
||||||
|
|
||||||
buffer.set_size(
|
buffer.set_size(
|
||||||
&mut font_system,
|
font_system.raw(),
|
||||||
text.bounds.width,
|
text.bounds.width,
|
||||||
text.bounds.height,
|
text.bounds.height,
|
||||||
);
|
);
|
||||||
|
|
||||||
buffer.set_text(
|
buffer.set_text(
|
||||||
&mut font_system,
|
font_system.raw(),
|
||||||
text.content,
|
text.content,
|
||||||
text::to_attributes(text.font),
|
text::to_attributes(text.font),
|
||||||
text::to_shaping(text.shaping),
|
text::to_shaping(text.shaping),
|
||||||
|
|
@ -64,30 +99,11 @@ impl Paragraph {
|
||||||
shaping: text.shaping,
|
shaping: text.shaping,
|
||||||
bounds: text.bounds,
|
bounds: text.bounds,
|
||||||
min_bounds,
|
min_bounds,
|
||||||
version,
|
version: font_system.version(),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn buffer(&self) -> &cosmic_text::Buffer {
|
fn resize(&mut self, new_bounds: Size) {
|
||||||
&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) {
|
|
||||||
let paragraph = self
|
let paragraph = self
|
||||||
.0
|
.0
|
||||||
.take()
|
.take()
|
||||||
|
|
@ -95,10 +111,11 @@ impl Paragraph {
|
||||||
|
|
||||||
match Arc::try_unwrap(paragraph) {
|
match Arc::try_unwrap(paragraph) {
|
||||||
Ok(mut internal) => {
|
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(
|
internal.buffer.set_size(
|
||||||
&mut font_system,
|
font_system.raw(),
|
||||||
new_bounds.width,
|
new_bounds.width,
|
||||||
new_bounds.height,
|
new_bounds.height,
|
||||||
);
|
);
|
||||||
|
|
@ -113,55 +130,42 @@ impl Paragraph {
|
||||||
|
|
||||||
// If there is a strong reference somewhere, we recompute the
|
// If there is a strong reference somewhere, we recompute the
|
||||||
// buffer from scratch
|
// buffer from scratch
|
||||||
*self = Self::with_text(
|
*self = Self::with_text(Text {
|
||||||
Text {
|
content: &internal.content,
|
||||||
content: &internal.content,
|
bounds: internal.bounds,
|
||||||
bounds: internal.bounds,
|
size: Pixels(metrics.font_size),
|
||||||
size: Pixels(metrics.font_size),
|
line_height: LineHeight::Absolute(Pixels(
|
||||||
line_height: LineHeight::Absolute(Pixels(
|
metrics.line_height,
|
||||||
metrics.line_height,
|
)),
|
||||||
)),
|
font: internal.font,
|
||||||
font: internal.font,
|
horizontal_alignment: internal.horizontal_alignment,
|
||||||
horizontal_alignment: internal.horizontal_alignment,
|
vertical_alignment: internal.vertical_alignment,
|
||||||
vertical_alignment: internal.vertical_alignment,
|
shaping: internal.shaping,
|
||||||
shaping: internal.shaping,
|
});
|
||||||
},
|
|
||||||
font_system,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn internal(&self) -> &Arc<Internal> {
|
fn compare(&self, text: Text<'_, Font>) -> core::text::Difference {
|
||||||
self.0
|
let font_system = text::font_system().read().expect("Read font system");
|
||||||
.as_ref()
|
let paragraph = self.internal();
|
||||||
.expect("paragraph should always be initialized")
|
let metrics = paragraph.buffer.metrics();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::text::Paragraph for Paragraph {
|
if paragraph.version != font_system.version
|
||||||
type Font = Font;
|
|| paragraph.content != text.content
|
||||||
|
|| metrics.font_size != text.size.0
|
||||||
fn content(&self) -> &str {
|
|| metrics.line_height != text.line_height.to_absolute(text.size).0
|
||||||
&self.internal().content
|
|| paragraph.font != text.font
|
||||||
}
|
|| paragraph.shaping != text.shaping
|
||||||
|
|| paragraph.horizontal_alignment != text.horizontal_alignment
|
||||||
fn text_size(&self) -> Pixels {
|
|| paragraph.vertical_alignment != text.vertical_alignment
|
||||||
Pixels(self.internal().buffer.metrics().font_size)
|
{
|
||||||
}
|
core::text::Difference::Shape
|
||||||
|
} else if paragraph.bounds != text.bounds {
|
||||||
fn line_height(&self) -> LineHeight {
|
core::text::Difference::Bounds
|
||||||
LineHeight::Absolute(Pixels(
|
} else {
|
||||||
self.internal().buffer.metrics().line_height,
|
core::text::Difference::None
|
||||||
))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn font(&self) -> Font {
|
|
||||||
self.internal().font
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shaping(&self) -> Shaping {
|
|
||||||
self.internal().shaping
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn horizontal_alignment(&self) -> alignment::Horizontal {
|
fn horizontal_alignment(&self) -> alignment::Horizontal {
|
||||||
|
|
@ -172,10 +176,6 @@ impl core::text::Paragraph for Paragraph {
|
||||||
self.internal().vertical_alignment
|
self.internal().vertical_alignment
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bounds(&self) -> Size {
|
|
||||||
self.internal().bounds
|
|
||||||
}
|
|
||||||
|
|
||||||
fn min_bounds(&self) -> Size {
|
fn min_bounds(&self) -> Size {
|
||||||
self.internal().min_bounds
|
self.internal().min_bounds
|
||||||
}
|
}
|
||||||
|
|
@ -278,15 +278,20 @@ impl Default for Internal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A weak reference to a [`Paragraph`].
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Weak {
|
pub struct Weak {
|
||||||
raw: sync::Weak<Internal>,
|
raw: sync::Weak<Internal>,
|
||||||
|
/// The minimum bounds of the [`Paragraph`].
|
||||||
pub min_bounds: Size,
|
pub min_bounds: Size,
|
||||||
|
/// The horizontal alignment of the [`Paragraph`].
|
||||||
pub horizontal_alignment: alignment::Horizontal,
|
pub horizontal_alignment: alignment::Horizontal,
|
||||||
|
/// The vertical alignment of the [`Paragraph`].
|
||||||
pub vertical_alignment: alignment::Vertical,
|
pub vertical_alignment: alignment::Vertical,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Weak {
|
impl Weak {
|
||||||
|
/// Tries to update the reference into a [`Paragraph`].
|
||||||
pub fn upgrade(&self) -> Option<Paragraph> {
|
pub fn upgrade(&self) -> Option<Paragraph> {
|
||||||
self.raw.upgrade().map(Some).map(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::renderer;
|
||||||
use crate::core::text::{self, Text};
|
use crate::core::text::{self, Text};
|
||||||
use crate::core::{
|
use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
|
||||||
Background, Color, Font, Pixels, Point, Rectangle, Size, Vector,
|
use crate::graphics::text::Editor;
|
||||||
};
|
|
||||||
use crate::graphics::text::Paragraph;
|
use crate::graphics::text::Paragraph;
|
||||||
use crate::graphics::Mesh;
|
use crate::graphics::Mesh;
|
||||||
|
|
||||||
|
|
@ -149,6 +148,7 @@ impl<T> core::Renderer for Renderer<T> {
|
||||||
impl<T> text::Renderer for Renderer<T> {
|
impl<T> text::Renderer for Renderer<T> {
|
||||||
type Font = Font;
|
type Font = Font;
|
||||||
type Paragraph = Paragraph;
|
type Paragraph = Paragraph;
|
||||||
|
type Editor = Editor;
|
||||||
|
|
||||||
const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;
|
const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;
|
||||||
const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON;
|
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())
|
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]>) {
|
fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
|
||||||
delegate!(self, renderer, renderer.load_font(bytes));
|
delegate!(self, renderer, renderer.load_font(bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_paragraph(
|
fn fill_paragraph(
|
||||||
&mut self,
|
&mut self,
|
||||||
text: &Self::Paragraph,
|
paragraph: &Self::Paragraph,
|
||||||
position: Point,
|
position: Point,
|
||||||
color: Color,
|
color: Color,
|
||||||
) {
|
) {
|
||||||
delegate!(
|
delegate!(
|
||||||
self,
|
self,
|
||||||
renderer,
|
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> {
|
impl<T> crate::core::image::Renderer for Renderer<T> {
|
||||||
type Handle = crate::core::image::Handle;
|
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))
|
delegate!(self, renderer, renderer.dimensions(handle))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,7 +221,7 @@ impl<T> crate::core::image::Renderer for Renderer<T> {
|
||||||
|
|
||||||
#[cfg(feature = "svg")]
|
#[cfg(feature = "svg")]
|
||||||
impl<T> crate::core::svg::Renderer for Renderer<T> {
|
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))
|
delegate!(self, renderer, renderer.dimensions(handle))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,9 @@ use iced_winit::runtime;
|
||||||
|
|
||||||
pub use iced_futures::futures;
|
pub use iced_futures::futures;
|
||||||
|
|
||||||
|
#[cfg(feature = "highlighter")]
|
||||||
|
pub use iced_highlighter as highlighter;
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod sandbox;
|
mod sandbox;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
use crate::window;
|
use crate::window;
|
||||||
use crate::{Font, Pixels};
|
use crate::{Font, Pixels};
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
/// The settings of an application.
|
/// The settings of an application.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Settings<Flags> {
|
pub struct Settings<Flags> {
|
||||||
|
|
@ -21,6 +23,9 @@ pub struct Settings<Flags> {
|
||||||
/// [`Application`]: crate::Application
|
/// [`Application`]: crate::Application
|
||||||
pub flags: Flags,
|
pub flags: Flags,
|
||||||
|
|
||||||
|
/// The fonts to load on boot.
|
||||||
|
pub fonts: Vec<Cow<'static, [u8]>>,
|
||||||
|
|
||||||
/// The default [`Font`] to be used.
|
/// The default [`Font`] to be used.
|
||||||
///
|
///
|
||||||
/// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif).
|
/// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif).
|
||||||
|
|
@ -62,6 +67,7 @@ impl<Flags> Settings<Flags> {
|
||||||
flags,
|
flags,
|
||||||
id: default_settings.id,
|
id: default_settings.id,
|
||||||
window: default_settings.window,
|
window: default_settings.window,
|
||||||
|
fonts: default_settings.fonts,
|
||||||
default_font: default_settings.default_font,
|
default_font: default_settings.default_font,
|
||||||
default_text_size: default_settings.default_text_size,
|
default_text_size: default_settings.default_text_size,
|
||||||
antialiasing: default_settings.antialiasing,
|
antialiasing: default_settings.antialiasing,
|
||||||
|
|
@ -79,6 +85,7 @@ where
|
||||||
id: None,
|
id: None,
|
||||||
window: window::Settings::default(),
|
window: window::Settings::default(),
|
||||||
flags: Default::default(),
|
flags: Default::default(),
|
||||||
|
fonts: Vec::new(),
|
||||||
default_font: Font::default(),
|
default_font: Font::default(),
|
||||||
default_text_size: Pixels(16.0),
|
default_text_size: Pixels(16.0),
|
||||||
antialiasing: false,
|
antialiasing: false,
|
||||||
|
|
@ -93,6 +100,7 @@ impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> {
|
||||||
id: settings.id,
|
id: settings.id,
|
||||||
window: settings.window.into(),
|
window: settings.window.into(),
|
||||||
flags: settings.flags,
|
flags: settings.flags,
|
||||||
|
fonts: settings.fonts,
|
||||||
exit_on_close_request: settings.exit_on_close_request,
|
exit_on_close_request: settings.exit_on_close_request,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ pub mod rule;
|
||||||
pub mod scrollable;
|
pub mod scrollable;
|
||||||
pub mod slider;
|
pub mod slider;
|
||||||
pub mod svg;
|
pub mod svg;
|
||||||
|
pub mod text_editor;
|
||||||
pub mod text_input;
|
pub mod text_input;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod toggler;
|
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::scrollable;
|
||||||
use crate::slider;
|
use crate::slider;
|
||||||
use crate::svg;
|
use crate::svg;
|
||||||
|
use crate::text_editor;
|
||||||
use crate::text_input;
|
use crate::text_input;
|
||||||
use crate::toggler;
|
use crate::toggler;
|
||||||
|
|
||||||
|
|
@ -1174,3 +1175,115 @@ impl text_input::StyleSheet for Theme {
|
||||||
self.placeholder_color(style)
|
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::core::{Background, Color, Gradient, Rectangle, Vector};
|
||||||
use crate::graphics::backend;
|
use crate::graphics::backend;
|
||||||
use crate::graphics::text;
|
|
||||||
use crate::graphics::{Damage, Viewport};
|
use crate::graphics::{Damage, Viewport};
|
||||||
use crate::primitive::{self, Primitive};
|
use crate::primitive::{self, Primitive};
|
||||||
|
|
||||||
|
|
@ -384,6 +383,31 @@ impl Backend {
|
||||||
clip_mask,
|
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 {
|
Primitive::Text {
|
||||||
content,
|
content,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -803,10 +827,6 @@ impl iced_graphics::Backend for Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl backend::Text 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]>) {
|
fn load_font(&mut self, font: Cow<'static, [u8]>) {
|
||||||
self.text_pipeline.load_font(font);
|
self.text_pipeline.load_font(font);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
use crate::core::alignment;
|
use crate::core::alignment;
|
||||||
use crate::core::text::{LineHeight, Shaping};
|
use crate::core::text::{LineHeight, Shaping};
|
||||||
use crate::core::{Color, Font, Pixels, Point, Rectangle};
|
use crate::core::{Color, Font, Pixels, Point, Rectangle};
|
||||||
|
use crate::graphics::color;
|
||||||
use crate::graphics::text::cache::{self, Cache};
|
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::paragraph;
|
||||||
use crate::graphics::text::FontSystem;
|
|
||||||
|
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
@ -12,7 +14,6 @@ use std::collections::hash_map;
|
||||||
|
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Pipeline {
|
pub struct Pipeline {
|
||||||
font_system: FontSystem,
|
|
||||||
glyph_cache: GlyphCache,
|
glyph_cache: GlyphCache,
|
||||||
cache: RefCell<Cache>,
|
cache: RefCell<Cache>,
|
||||||
}
|
}
|
||||||
|
|
@ -20,18 +21,16 @@ pub struct Pipeline {
|
||||||
impl Pipeline {
|
impl Pipeline {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Pipeline {
|
Pipeline {
|
||||||
font_system: FontSystem::new(),
|
|
||||||
glyph_cache: GlyphCache::new(),
|
glyph_cache: GlyphCache::new(),
|
||||||
cache: RefCell::new(Cache::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]>) {
|
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());
|
self.cache = RefCell::new(Cache::new());
|
||||||
}
|
}
|
||||||
|
|
@ -51,8 +50,10 @@ impl Pipeline {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut font_system = font_system().write().expect("Write font system");
|
||||||
|
|
||||||
draw(
|
draw(
|
||||||
self.font_system.get_mut(),
|
font_system.raw(),
|
||||||
&mut self.glyph_cache,
|
&mut self.glyph_cache,
|
||||||
paragraph.buffer(),
|
paragraph.buffer(),
|
||||||
Rectangle::new(position, paragraph.min_bounds()),
|
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(
|
pub fn draw_cached(
|
||||||
&mut self,
|
&mut self,
|
||||||
content: &str,
|
content: &str,
|
||||||
|
|
@ -82,7 +114,9 @@ impl Pipeline {
|
||||||
) {
|
) {
|
||||||
let line_height = f32::from(line_height.to_absolute(size));
|
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 {
|
let key = cache::Key {
|
||||||
bounds: bounds.size(),
|
bounds: bounds.size(),
|
||||||
content,
|
content,
|
||||||
|
|
@ -155,7 +189,7 @@ fn draw(
|
||||||
|
|
||||||
if let Some((buffer, placement)) = glyph_cache.allocate(
|
if let Some((buffer, placement)) = glyph_cache.allocate(
|
||||||
physical_glyph.cache_key,
|
physical_glyph.cache_key,
|
||||||
color,
|
glyph.color_opt.map(from_color).unwrap_or(color),
|
||||||
font_system,
|
font_system,
|
||||||
&mut swash,
|
&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)]
|
#[derive(Debug, Clone, Default)]
|
||||||
struct GlyphCache {
|
struct GlyphCache {
|
||||||
entries: FxHashMap<
|
entries: FxHashMap<
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::core::{Color, Size};
|
use crate::core::{Color, Size};
|
||||||
use crate::graphics;
|
|
||||||
use crate::graphics::backend;
|
use crate::graphics::backend;
|
||||||
use crate::graphics::color;
|
use crate::graphics::color;
|
||||||
use crate::graphics::{Transformation, Viewport};
|
use crate::graphics::{Transformation, Viewport};
|
||||||
|
|
@ -314,10 +313,6 @@ impl crate::graphics::Backend for Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl backend::Text 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]>) {
|
fn load_font(&mut self, font: Cow<'static, [u8]>) {
|
||||||
self.text_pipeline.load_font(font);
|
self.text_pipeline.load_font(font);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,12 +120,25 @@ impl<'a> Layer<'a> {
|
||||||
} => {
|
} => {
|
||||||
let layer = &mut layers[current_layer];
|
let layer = &mut layers[current_layer];
|
||||||
|
|
||||||
layer.text.push(Text::Managed {
|
layer.text.push(Text::Paragraph {
|
||||||
paragraph: paragraph.clone(),
|
paragraph: paragraph.clone(),
|
||||||
position: *position + translation,
|
position: *position + translation,
|
||||||
color: *color,
|
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 {
|
Primitive::Text {
|
||||||
content,
|
content,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,27 @@
|
||||||
use crate::core::alignment;
|
use crate::core::alignment;
|
||||||
use crate::core::text;
|
use crate::core::text;
|
||||||
use crate::core::{Color, Font, Pixels, Point, Rectangle};
|
use crate::core::{Color, Font, Pixels, Point, Rectangle};
|
||||||
|
use crate::graphics::text::editor;
|
||||||
use crate::graphics::text::paragraph;
|
use crate::graphics::text::paragraph;
|
||||||
|
|
||||||
/// A paragraph of text.
|
/// A text primitive.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Text<'a> {
|
pub enum Text<'a> {
|
||||||
Managed {
|
/// A paragraph.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
Paragraph {
|
||||||
paragraph: paragraph::Weak,
|
paragraph: paragraph::Weak,
|
||||||
position: Point,
|
position: Point,
|
||||||
color: Color,
|
color: Color,
|
||||||
},
|
},
|
||||||
|
/// An editor.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
Editor {
|
||||||
|
editor: editor::Weak,
|
||||||
|
position: Point,
|
||||||
|
color: Color,
|
||||||
|
},
|
||||||
|
/// A cached text.
|
||||||
Cached(Cached<'a>),
|
Cached(Cached<'a>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
#![forbid(rust_2018_idioms)]
|
#![forbid(rust_2018_idioms)]
|
||||||
#![deny(
|
#![deny(
|
||||||
missing_debug_implementations,
|
missing_debug_implementations,
|
||||||
//missing_docs,
|
missing_docs,
|
||||||
unsafe_code,
|
unsafe_code,
|
||||||
unused_results,
|
unused_results,
|
||||||
rustdoc::broken_intra_doc_links
|
rustdoc::broken_intra_doc_links
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::core::alignment;
|
||||||
use crate::core::{Rectangle, Size};
|
use crate::core::{Rectangle, Size};
|
||||||
use crate::graphics::color;
|
use crate::graphics::color;
|
||||||
use crate::graphics::text::cache::{self, Cache};
|
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 crate::layer::Text;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
@ -10,7 +10,6 @@ use std::cell::RefCell;
|
||||||
|
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Pipeline {
|
pub struct Pipeline {
|
||||||
font_system: FontSystem,
|
|
||||||
renderers: Vec<glyphon::TextRenderer>,
|
renderers: Vec<glyphon::TextRenderer>,
|
||||||
atlas: glyphon::TextAtlas,
|
atlas: glyphon::TextAtlas,
|
||||||
prepare_layer: usize,
|
prepare_layer: usize,
|
||||||
|
|
@ -24,7 +23,6 @@ impl Pipeline {
|
||||||
format: wgpu::TextureFormat,
|
format: wgpu::TextureFormat,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Pipeline {
|
Pipeline {
|
||||||
font_system: FontSystem::new(),
|
|
||||||
renderers: Vec::new(),
|
renderers: Vec::new(),
|
||||||
atlas: glyphon::TextAtlas::with_color_mode(
|
atlas: glyphon::TextAtlas::with_color_mode(
|
||||||
device,
|
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]>) {
|
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());
|
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 renderer = &mut self.renderers[self.prepare_layer];
|
||||||
let cache = self.cache.get_mut();
|
let cache = self.cache.get_mut();
|
||||||
|
|
||||||
enum Allocation {
|
enum Allocation {
|
||||||
Paragraph(Paragraph),
|
Paragraph(Paragraph),
|
||||||
|
Editor(Editor),
|
||||||
Cache(cache::KeyHash),
|
Cache(cache::KeyHash),
|
||||||
}
|
}
|
||||||
|
|
||||||
let allocations: Vec<_> = sections
|
let allocations: Vec<_> = sections
|
||||||
.iter()
|
.iter()
|
||||||
.map(|section| match section {
|
.map(|section| match section {
|
||||||
Text::Managed { paragraph, .. } => {
|
Text::Paragraph { paragraph, .. } => {
|
||||||
paragraph.upgrade().map(Allocation::Paragraph)
|
paragraph.upgrade().map(Allocation::Paragraph)
|
||||||
}
|
}
|
||||||
|
Text::Editor { editor, .. } => {
|
||||||
|
editor.upgrade().map(Allocation::Editor)
|
||||||
|
}
|
||||||
Text::Cached(text) => {
|
Text::Cached(text) => {
|
||||||
let (key, _) = cache.allocate(
|
let (key, _) = cache.allocate(
|
||||||
font_system,
|
font_system,
|
||||||
|
|
@ -118,7 +121,7 @@ impl Pipeline {
|
||||||
vertical_alignment,
|
vertical_alignment,
|
||||||
color,
|
color,
|
||||||
) = match section {
|
) = match section {
|
||||||
Text::Managed {
|
Text::Paragraph {
|
||||||
position, color, ..
|
position, color, ..
|
||||||
} => {
|
} => {
|
||||||
use crate::core::text::Paragraph as _;
|
use crate::core::text::Paragraph as _;
|
||||||
|
|
@ -136,6 +139,24 @@ impl Pipeline {
|
||||||
*color,
|
*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) => {
|
Text::Cached(text) => {
|
||||||
let Some(Allocation::Cache(key)) = allocation else {
|
let Some(Allocation::Cache(key)) = allocation else {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -193,16 +214,7 @@ impl Pipeline {
|
||||||
right: (clip_bounds.x + clip_bounds.width) as i32,
|
right: (clip_bounds.x + clip_bounds.width) as i32,
|
||||||
bottom: (clip_bounds.y + clip_bounds.height) as i32,
|
bottom: (clip_bounds.y + clip_bounds.height) as i32,
|
||||||
},
|
},
|
||||||
default_color: {
|
default_color: to_color(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,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use crate::runtime::Command;
|
||||||
use crate::scrollable::{self, Scrollable};
|
use crate::scrollable::{self, Scrollable};
|
||||||
use crate::slider::{self, Slider};
|
use crate::slider::{self, Slider};
|
||||||
use crate::text::{self, Text};
|
use crate::text::{self, Text};
|
||||||
|
use crate::text_editor::{self, TextEditor};
|
||||||
use crate::text_input::{self, TextInput};
|
use crate::text_input::{self, TextInput};
|
||||||
use crate::toggler::{self, Toggler};
|
use crate::toggler::{self, Toggler};
|
||||||
use crate::tooltip::{self, Tooltip};
|
use crate::tooltip::{self, Tooltip};
|
||||||
|
|
@ -206,6 +207,20 @@ where
|
||||||
TextInput::new(placeholder, value)
|
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`].
|
/// Creates a new [`Slider`].
|
||||||
///
|
///
|
||||||
/// [`Slider`]: crate::Slider
|
/// [`Slider`]: crate::Slider
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ pub mod scrollable;
|
||||||
pub mod slider;
|
pub mod slider;
|
||||||
pub mod space;
|
pub mod space;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
pub mod text_editor;
|
||||||
pub mod text_input;
|
pub mod text_input;
|
||||||
pub mod toggler;
|
pub mod toggler;
|
||||||
pub mod tooltip;
|
pub mod tooltip;
|
||||||
|
|
@ -86,6 +87,8 @@ pub use space::Space;
|
||||||
#[doc(no_inline)]
|
#[doc(no_inline)]
|
||||||
pub use text::Text;
|
pub use text::Text;
|
||||||
#[doc(no_inline)]
|
#[doc(no_inline)]
|
||||||
|
pub use text_editor::TextEditor;
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use text_input::TextInput;
|
pub use text_input::TextInput;
|
||||||
#[doc(no_inline)]
|
#[doc(no_inline)]
|
||||||
pub use toggler::Toggler;
|
pub use toggler::Toggler;
|
||||||
|
|
|
||||||
|
|
@ -415,23 +415,17 @@ where
|
||||||
for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
|
for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
|
||||||
let label = option.to_string();
|
let label = option.to_string();
|
||||||
|
|
||||||
renderer.update_paragraph(
|
paragraph.update(Text {
|
||||||
paragraph,
|
content: &label,
|
||||||
Text {
|
..option_text
|
||||||
content: &label,
|
});
|
||||||
..option_text
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(placeholder) = placeholder {
|
if let Some(placeholder) = placeholder {
|
||||||
renderer.update_paragraph(
|
state.placeholder.update(Text {
|
||||||
&mut state.placeholder,
|
content: placeholder,
|
||||||
Text {
|
..option_text
|
||||||
content: placeholder,
|
});
|
||||||
..option_text
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_width = match width {
|
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,
|
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 secure_value = is_secure.then(|| value.secure());
|
||||||
let value = secure_value.as_ref().unwrap_or(value);
|
let value = secure_value.as_ref().unwrap_or(value);
|
||||||
|
|
||||||
renderer.update_paragraph(
|
state.value.update(Text {
|
||||||
&mut state.value,
|
content: &value.to_string(),
|
||||||
Text {
|
..placeholder_text
|
||||||
content: &value.to_string(),
|
});
|
||||||
..placeholder_text
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(icon) = icon {
|
if let Some(icon) = icon {
|
||||||
let icon_text = Text {
|
let icon_text = Text {
|
||||||
|
|
@ -548,7 +545,7 @@ where
|
||||||
shaping: text::Shaping::Advanced,
|
shaping: text::Shaping::Advanced,
|
||||||
};
|
};
|
||||||
|
|
||||||
renderer.update_paragraph(&mut state.icon, icon_text);
|
state.icon.update(icon_text);
|
||||||
|
|
||||||
let icon_width = state.icon.min_width();
|
let icon_width = state.icon.min_width();
|
||||||
|
|
||||||
|
|
@ -1461,7 +1458,7 @@ fn replace_paragraph<Renderer>(
|
||||||
let mut children_layout = layout.children();
|
let mut children_layout = layout.children();
|
||||||
let text_bounds = children_layout.next().unwrap().bounds();
|
let text_bounds = children_layout.next().unwrap().bounds();
|
||||||
|
|
||||||
state.value = renderer.create_paragraph(Text {
|
state.value = Renderer::Paragraph::with_text(Text {
|
||||||
font,
|
font,
|
||||||
line_height,
|
line_height,
|
||||||
content: &value.to_string(),
|
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 (mut event_sender, event_receiver) = mpsc::unbounded();
|
||||||
let (control_sender, mut control_receiver) = mpsc::unbounded();
|
let (control_sender, mut control_receiver) = mpsc::unbounded();
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ use crate::Position;
|
||||||
use winit::monitor::MonitorHandle;
|
use winit::monitor::MonitorHandle;
|
||||||
use winit::window::WindowBuilder;
|
use winit::window::WindowBuilder;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
/// The settings of an application.
|
/// The settings of an application.
|
||||||
|
|
@ -52,6 +53,9 @@ pub struct Settings<Flags> {
|
||||||
/// [`Application`]: crate::Application
|
/// [`Application`]: crate::Application
|
||||||
pub flags: Flags,
|
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
|
/// Whether the [`Application`] should exit when the user requests the
|
||||||
/// window to close (e.g. the user presses the close button).
|
/// window to close (e.g. the user presses the close button).
|
||||||
///
|
///
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue