Merge pull request #2776 from iced-rs/fix/markdown
Incremental `markdown` parsing and various fixes
This commit is contained in:
commit
91f94f3b6e
11 changed files with 547 additions and 168 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2640,6 +2640,7 @@ dependencies = [
|
||||||
"iced_highlighter",
|
"iced_highlighter",
|
||||||
"iced_renderer",
|
"iced_renderer",
|
||||||
"iced_runtime",
|
"iced_runtime",
|
||||||
|
"log",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"ouroboros",
|
"ouroboros",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,6 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["markdown", "highlighter", "debug"]
|
iced.features = ["markdown", "highlighter", "tokio", "debug"]
|
||||||
|
|
||||||
open = "5.3"
|
open = "5.3"
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,37 @@
|
||||||
use iced::highlighter;
|
use iced::highlighter;
|
||||||
use iced::widget::{self, markdown, row, scrollable, text_editor};
|
use iced::time::{self, milliseconds};
|
||||||
use iced::{Element, Fill, Font, Task, Theme};
|
use iced::widget::{
|
||||||
|
self, hover, markdown, right, row, scrollable, text_editor, toggler,
|
||||||
|
};
|
||||||
|
use iced::{Element, Fill, Font, Subscription, Task, Theme};
|
||||||
|
|
||||||
pub fn main() -> iced::Result {
|
pub fn main() -> iced::Result {
|
||||||
iced::application("Markdown - Iced", Markdown::update, Markdown::view)
|
iced::application("Markdown - Iced", Markdown::update, Markdown::view)
|
||||||
|
.subscription(Markdown::subscription)
|
||||||
.theme(Markdown::theme)
|
.theme(Markdown::theme)
|
||||||
.run_with(Markdown::new)
|
.run_with(Markdown::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Markdown {
|
struct Markdown {
|
||||||
content: text_editor::Content,
|
content: text_editor::Content,
|
||||||
items: Vec<markdown::Item>,
|
mode: Mode,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Mode {
|
||||||
|
Preview(Vec<markdown::Item>),
|
||||||
|
Stream {
|
||||||
|
pending: String,
|
||||||
|
parsed: markdown::Content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Message {
|
enum Message {
|
||||||
Edit(text_editor::Action),
|
Edit(text_editor::Action),
|
||||||
LinkClicked(markdown::Url),
|
LinkClicked(markdown::Url),
|
||||||
|
ToggleStream(bool),
|
||||||
|
NextToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Markdown {
|
impl Markdown {
|
||||||
|
|
@ -29,14 +43,14 @@ impl Markdown {
|
||||||
(
|
(
|
||||||
Self {
|
Self {
|
||||||
content: text_editor::Content::with_text(INITIAL_CONTENT),
|
content: text_editor::Content::with_text(INITIAL_CONTENT),
|
||||||
items: markdown::parse(INITIAL_CONTENT).collect(),
|
mode: Mode::Preview(markdown::parse(INITIAL_CONTENT).collect()),
|
||||||
theme,
|
theme,
|
||||||
},
|
},
|
||||||
widget::focus_next(),
|
widget::focus_next(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, message: Message) {
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
match message {
|
match message {
|
||||||
Message::Edit(action) => {
|
Message::Edit(action) => {
|
||||||
let is_edit = action.is_edit();
|
let is_edit = action.is_edit();
|
||||||
|
|
@ -44,12 +58,56 @@ impl Markdown {
|
||||||
self.content.perform(action);
|
self.content.perform(action);
|
||||||
|
|
||||||
if is_edit {
|
if is_edit {
|
||||||
self.items =
|
self.mode = Mode::Preview(
|
||||||
markdown::parse(&self.content.text()).collect();
|
markdown::parse(&self.content.text()).collect(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::LinkClicked(link) => {
|
Message::LinkClicked(link) => {
|
||||||
let _ = open::that_in_background(link.to_string());
|
let _ = open::that_in_background(link.to_string());
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ToggleStream(enable_stream) => {
|
||||||
|
if enable_stream {
|
||||||
|
self.mode = Mode::Stream {
|
||||||
|
pending: self.content.text(),
|
||||||
|
parsed: markdown::Content::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollable::snap_to(
|
||||||
|
"preview",
|
||||||
|
scrollable::RelativeOffset::END,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.mode = Mode::Preview(
|
||||||
|
markdown::parse(&self.content.text()).collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::NextToken => {
|
||||||
|
match &mut self.mode {
|
||||||
|
Mode::Preview(_) => {}
|
||||||
|
Mode::Stream { pending, parsed } => {
|
||||||
|
if pending.is_empty() {
|
||||||
|
self.mode = Mode::Preview(parsed.items().to_vec());
|
||||||
|
} else {
|
||||||
|
let mut tokens = pending.split(' ');
|
||||||
|
|
||||||
|
if let Some(token) = tokens.next() {
|
||||||
|
parsed.push_str(&format!("{token} "));
|
||||||
|
}
|
||||||
|
|
||||||
|
*pending = tokens.collect::<Vec<_>>().join(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -63,20 +121,49 @@ impl Markdown {
|
||||||
.font(Font::MONOSPACE)
|
.font(Font::MONOSPACE)
|
||||||
.highlight("markdown", highlighter::Theme::Base16Ocean);
|
.highlight("markdown", highlighter::Theme::Base16Ocean);
|
||||||
|
|
||||||
|
let items = match &self.mode {
|
||||||
|
Mode::Preview(items) => items.as_slice(),
|
||||||
|
Mode::Stream { parsed, .. } => parsed.items(),
|
||||||
|
};
|
||||||
|
|
||||||
let preview = markdown(
|
let preview = markdown(
|
||||||
&self.items,
|
items,
|
||||||
markdown::Settings::default(),
|
markdown::Settings::default(),
|
||||||
markdown::Style::from_palette(self.theme.palette()),
|
markdown::Style::from_palette(self.theme.palette()),
|
||||||
)
|
)
|
||||||
.map(Message::LinkClicked);
|
.map(Message::LinkClicked);
|
||||||
|
|
||||||
row![editor, scrollable(preview).spacing(10).height(Fill)]
|
row![
|
||||||
.spacing(10)
|
editor,
|
||||||
.padding(10)
|
hover(
|
||||||
.into()
|
scrollable(preview)
|
||||||
|
.spacing(10)
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.id("preview"),
|
||||||
|
right(
|
||||||
|
toggler(matches!(self.mode, Mode::Stream { .. }))
|
||||||
|
.label("Stream")
|
||||||
|
.on_toggle(Message::ToggleStream)
|
||||||
|
)
|
||||||
|
.padding([0, 20])
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.padding(10)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn theme(&self) -> Theme {
|
fn theme(&self) -> Theme {
|
||||||
self.theme.clone()
|
self.theme.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Preview(_) => Subscription::none(),
|
||||||
|
Mode::Stream { .. } => {
|
||||||
|
time::every(milliseconds(20)).map(|_| Message::NextToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::core::Color;
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use syntect::highlighting;
|
use syntect::highlighting;
|
||||||
use syntect::parsing;
|
use syntect::parsing;
|
||||||
|
|
||||||
|
|
@ -104,30 +105,7 @@ impl highlighter::Highlighter for Highlighter {
|
||||||
|
|
||||||
let ops = parser.parse_line(line, &SYNTAXES).unwrap_or_default();
|
let ops = parser.parse_line(line, &SYNTAXES).unwrap_or_default();
|
||||||
|
|
||||||
let highlighter = &self.highlighter;
|
Box::new(scope_iterator(ops, line, stack, &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 {
|
fn current_line(&self) -> usize {
|
||||||
|
|
@ -135,6 +113,92 @@ impl highlighter::Highlighter for Highlighter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scope_iterator<'a>(
|
||||||
|
ops: Vec<(usize, parsing::ScopeStackOp)>,
|
||||||
|
line: &str,
|
||||||
|
stack: &'a mut parsing::ScopeStack,
|
||||||
|
highlighter: &'a highlighting::Highlighter<'static>,
|
||||||
|
) -> impl Iterator<Item = (Range<usize>, Highlight)> + 'a {
|
||||||
|
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)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A streaming syntax highlighter.
|
||||||
|
///
|
||||||
|
/// It can efficiently highlight an immutable stream of tokens.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Stream {
|
||||||
|
syntax: &'static parsing::SyntaxReference,
|
||||||
|
highlighter: highlighting::Highlighter<'static>,
|
||||||
|
commit: (parsing::ParseState, parsing::ScopeStack),
|
||||||
|
state: parsing::ParseState,
|
||||||
|
stack: parsing::ScopeStack,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream {
|
||||||
|
/// Creates a new [`Stream`] highlighter.
|
||||||
|
pub fn new(settings: &Settings) -> Self {
|
||||||
|
let syntax = SYNTAXES
|
||||||
|
.find_syntax_by_token(&settings.token)
|
||||||
|
.unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
|
||||||
|
|
||||||
|
let highlighter = highlighting::Highlighter::new(
|
||||||
|
&THEMES.themes[settings.theme.key()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let state = parsing::ParseState::new(syntax);
|
||||||
|
let stack = parsing::ScopeStack::new();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
syntax,
|
||||||
|
highlighter,
|
||||||
|
commit: (state.clone(), stack.clone()),
|
||||||
|
state,
|
||||||
|
stack,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highlights the given line from the last commit.
|
||||||
|
pub fn highlight_line(
|
||||||
|
&mut self,
|
||||||
|
line: &str,
|
||||||
|
) -> impl Iterator<Item = (Range<usize>, Highlight)> + '_ {
|
||||||
|
self.state = self.commit.0.clone();
|
||||||
|
self.stack = self.commit.1.clone();
|
||||||
|
|
||||||
|
let ops = self.state.parse_line(line, &SYNTAXES).unwrap_or_default();
|
||||||
|
scope_iterator(ops, line, &mut self.stack, &self.highlighter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commits the last highlighted line.
|
||||||
|
pub fn commit(&mut self) {
|
||||||
|
self.commit = (self.state.clone(), self.stack.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the [`Stream`] highlighter.
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.state = parsing::ParseState::new(self.syntax);
|
||||||
|
self.stack = parsing::ScopeStack::new();
|
||||||
|
self.commit = (self.state.clone(), self.stack.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The settings of a [`Highlighter`].
|
/// The settings of a [`Highlighter`].
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ iced_renderer.workspace = true
|
||||||
iced_runtime.workspace = true
|
iced_runtime.workspace = true
|
||||||
|
|
||||||
num-traits.workspace = true
|
num-traits.workspace = true
|
||||||
|
log.workspace = true
|
||||||
rustc-hash.workspace = true
|
rustc-hash.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
unicode-segmentation.workspace = true
|
unicode-segmentation.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -107,8 +107,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the [`Id`] of the [`Container`].
|
/// Sets the [`Id`] of the [`Container`].
|
||||||
pub fn id(mut self, id: Id) -> Self {
|
pub fn id(mut self, id: impl Into<Id>) -> Self {
|
||||||
self.id = Some(id);
|
self.id = Some(id.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -480,9 +480,17 @@ impl From<Id> for widget::Id {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for Id {
|
||||||
|
fn from(value: &'static str) -> Self {
|
||||||
|
Id::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Produces a [`Task`] that queries the visible screen bounds of the
|
/// Produces a [`Task`] that queries the visible screen bounds of the
|
||||||
/// [`Container`] with the given [`Id`].
|
/// [`Container`] with the given [`Id`].
|
||||||
pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
|
pub fn visible_bounds(id: impl Into<Id>) -> Task<Option<Rectangle>> {
|
||||||
|
let id = id.into();
|
||||||
|
|
||||||
struct VisibleBounds {
|
struct VisibleBounds {
|
||||||
target: widget::Id,
|
target: widget::Id,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
|
#![allow(missing_docs)]
|
||||||
use crate::core::border;
|
use crate::core::border;
|
||||||
use crate::core::font::{self, Font};
|
use crate::core::font::{self, Font};
|
||||||
use crate::core::padding;
|
use crate::core::padding;
|
||||||
|
|
@ -56,13 +57,55 @@ use crate::core::{
|
||||||
};
|
};
|
||||||
use crate::{column, container, rich_text, row, scrollable, span, text};
|
use crate::{column, container, rich_text, row, scrollable, span, text};
|
||||||
|
|
||||||
|
use std::borrow::BorrowMut;
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use core::text::Highlight;
|
pub use core::text::Highlight;
|
||||||
pub use pulldown_cmark::HeadingLevel;
|
pub use pulldown_cmark::HeadingLevel;
|
||||||
pub use url::Url;
|
pub use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Content {
|
||||||
|
items: Vec<Item>,
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Content {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(markdown: &str) -> Self {
|
||||||
|
let mut state = State::default();
|
||||||
|
let items = parse_with(&mut state, markdown).collect();
|
||||||
|
|
||||||
|
Self { items, state }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_str(&mut self, markdown: &str) {
|
||||||
|
if markdown.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append to last leftover text
|
||||||
|
let mut leftover = std::mem::take(&mut self.state.leftover);
|
||||||
|
leftover.push_str(markdown);
|
||||||
|
|
||||||
|
// Pop the last item
|
||||||
|
let _ = self.items.pop();
|
||||||
|
|
||||||
|
// Re-parse last item and new text
|
||||||
|
let new_items = parse_with(&mut self.state, &leftover);
|
||||||
|
self.items.extend(new_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn items(&self) -> &[Item] {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A Markdown item.
|
/// A Markdown item.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Item {
|
pub enum Item {
|
||||||
|
|
@ -73,7 +116,7 @@ pub enum Item {
|
||||||
/// A code block.
|
/// A code block.
|
||||||
///
|
///
|
||||||
/// You can enable the `highlighter` feature for syntax highlighting.
|
/// You can enable the `highlighter` feature for syntax highlighting.
|
||||||
CodeBlock(Text),
|
CodeBlock(Vec<Text>),
|
||||||
/// A list.
|
/// A list.
|
||||||
List {
|
List {
|
||||||
/// The first number of the list, if it is ordered.
|
/// The first number of the list, if it is ordered.
|
||||||
|
|
@ -232,12 +275,109 @@ impl Span {
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
|
parse_with(State::default(), markdown)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct State {
|
||||||
|
leftover: String,
|
||||||
|
#[cfg(feature = "highlighter")]
|
||||||
|
highlighter: Option<Highlighter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "highlighter")]
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Highlighter {
|
||||||
|
lines: Vec<(String, Vec<Span>)>,
|
||||||
|
language: String,
|
||||||
|
parser: iced_highlighter::Stream,
|
||||||
|
current: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "highlighter")]
|
||||||
|
impl Highlighter {
|
||||||
|
pub fn new(language: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
lines: Vec::new(),
|
||||||
|
parser: iced_highlighter::Stream::new(
|
||||||
|
&iced_highlighter::Settings {
|
||||||
|
theme: iced_highlighter::Theme::Base16Ocean,
|
||||||
|
token: language.to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
language: language.to_owned(),
|
||||||
|
current: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare(&mut self) {
|
||||||
|
self.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight_line(&mut self, text: &str) -> &[Span] {
|
||||||
|
match self.lines.get(self.current) {
|
||||||
|
Some(line) if line.0 == text => {}
|
||||||
|
_ => {
|
||||||
|
if self.current + 1 < self.lines.len() {
|
||||||
|
log::debug!("Resetting highlighter...");
|
||||||
|
self.parser.reset();
|
||||||
|
self.lines.truncate(self.current);
|
||||||
|
|
||||||
|
for line in &self.lines {
|
||||||
|
log::debug!(
|
||||||
|
"Refeeding {n} lines",
|
||||||
|
n = self.lines.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = self.parser.highlight_line(&line.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("Parsing: {text}", text = text.trim_end());
|
||||||
|
|
||||||
|
if self.current + 1 < self.lines.len() {
|
||||||
|
self.parser.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut spans = Vec::new();
|
||||||
|
|
||||||
|
for (range, highlight) in self.parser.highlight_line(text) {
|
||||||
|
spans.push(Span::Highlight {
|
||||||
|
text: text[range].to_owned(),
|
||||||
|
color: highlight.color(),
|
||||||
|
font: highlight.font(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.current + 1 == self.lines.len() {
|
||||||
|
let _ = self.lines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lines.push((text.to_owned(), spans));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.current += 1;
|
||||||
|
|
||||||
|
&self
|
||||||
|
.lines
|
||||||
|
.get(self.current - 1)
|
||||||
|
.expect("Line must be parsed")
|
||||||
|
.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_with<'a>(
|
||||||
|
mut state: impl BorrowMut<State> + 'a,
|
||||||
|
markdown: &'a str,
|
||||||
|
) -> impl Iterator<Item = Item> + 'a {
|
||||||
struct List {
|
struct List {
|
||||||
start: Option<u64>,
|
start: Option<u64>,
|
||||||
items: Vec<Vec<Item>>,
|
items: Vec<Vec<Item>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
|
let mut code = Vec::new();
|
||||||
let mut strong = false;
|
let mut strong = false;
|
||||||
let mut emphasis = false;
|
let mut emphasis = false;
|
||||||
let mut strikethrough = false;
|
let mut strikethrough = false;
|
||||||
|
|
@ -255,10 +395,16 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
| pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
|
| pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
|
||||||
| pulldown_cmark::Options::ENABLE_TABLES
|
| pulldown_cmark::Options::ENABLE_TABLES
|
||||||
| pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
|
| pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
|
||||||
);
|
)
|
||||||
|
.into_offset_iter();
|
||||||
|
|
||||||
let produce = |lists: &mut Vec<List>, item| {
|
let produce = move |state: &mut State,
|
||||||
|
lists: &mut Vec<List>,
|
||||||
|
item,
|
||||||
|
source: Range<usize>| {
|
||||||
if lists.is_empty() {
|
if lists.is_empty() {
|
||||||
|
state.leftover = markdown[source.start..].to_owned();
|
||||||
|
|
||||||
Some(item)
|
Some(item)
|
||||||
} else {
|
} else {
|
||||||
lists
|
lists
|
||||||
|
|
@ -275,7 +421,7 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
|
|
||||||
// We want to keep the `spans` capacity
|
// We want to keep the `spans` capacity
|
||||||
#[allow(clippy::drain_collect)]
|
#[allow(clippy::drain_collect)]
|
||||||
parser.filter_map(move |event| match event {
|
parser.filter_map(move |(event, source)| match event {
|
||||||
pulldown_cmark::Event::Start(tag) => match tag {
|
pulldown_cmark::Event::Start(tag) => match tag {
|
||||||
pulldown_cmark::Tag::Strong if !metadata && !table => {
|
pulldown_cmark::Tag::Strong if !metadata && !table => {
|
||||||
strong = true;
|
strong = true;
|
||||||
|
|
@ -309,8 +455,10 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
produce(
|
produce(
|
||||||
|
state.borrow_mut(),
|
||||||
&mut lists,
|
&mut lists,
|
||||||
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||||
|
source,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -334,17 +482,34 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
) if !metadata && !table => {
|
) if !metadata && !table => {
|
||||||
#[cfg(feature = "highlighter")]
|
#[cfg(feature = "highlighter")]
|
||||||
{
|
{
|
||||||
use iced_highlighter::Highlighter;
|
highlighter = Some({
|
||||||
use text::Highlighter as _;
|
let mut highlighter = state
|
||||||
|
.borrow_mut()
|
||||||
|
.highlighter
|
||||||
|
.take()
|
||||||
|
.filter(|highlighter| {
|
||||||
|
highlighter.language == _language.as_ref()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| Highlighter::new(&_language));
|
||||||
|
|
||||||
highlighter =
|
highlighter.prepare();
|
||||||
Some(Highlighter::new(&iced_highlighter::Settings {
|
|
||||||
theme: iced_highlighter::Theme::Base16Ocean,
|
highlighter
|
||||||
token: _language.to_string(),
|
});
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
let prev = if spans.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
produce(
|
||||||
|
state.borrow_mut(),
|
||||||
|
&mut lists,
|
||||||
|
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
prev
|
||||||
}
|
}
|
||||||
pulldown_cmark::Tag::MetadataBlock(_) => {
|
pulldown_cmark::Tag::MetadataBlock(_) => {
|
||||||
metadata = true;
|
metadata = true;
|
||||||
|
|
@ -359,8 +524,10 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
pulldown_cmark::Event::End(tag) => match tag {
|
pulldown_cmark::Event::End(tag) => match tag {
|
||||||
pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
|
pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
|
||||||
produce(
|
produce(
|
||||||
|
state.borrow_mut(),
|
||||||
&mut lists,
|
&mut lists,
|
||||||
Item::Heading(level, Text::new(spans.drain(..).collect())),
|
Item::Heading(level, Text::new(spans.drain(..).collect())),
|
||||||
|
source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pulldown_cmark::TagEnd::Strong if !metadata && !table => {
|
pulldown_cmark::TagEnd::Strong if !metadata && !table => {
|
||||||
|
|
@ -381,8 +548,10 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
}
|
}
|
||||||
pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
|
pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
|
||||||
produce(
|
produce(
|
||||||
|
state.borrow_mut(),
|
||||||
&mut lists,
|
&mut lists,
|
||||||
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||||
|
source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pulldown_cmark::TagEnd::Item if !metadata && !table => {
|
pulldown_cmark::TagEnd::Item if !metadata && !table => {
|
||||||
|
|
@ -390,8 +559,10 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
produce(
|
produce(
|
||||||
|
state.borrow_mut(),
|
||||||
&mut lists,
|
&mut lists,
|
||||||
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||||
|
source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -399,22 +570,26 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
let list = lists.pop().expect("list context");
|
let list = lists.pop().expect("list context");
|
||||||
|
|
||||||
produce(
|
produce(
|
||||||
|
state.borrow_mut(),
|
||||||
&mut lists,
|
&mut lists,
|
||||||
Item::List {
|
Item::List {
|
||||||
start: list.start,
|
start: list.start,
|
||||||
items: list.items,
|
items: list.items,
|
||||||
},
|
},
|
||||||
|
source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
|
pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
|
||||||
#[cfg(feature = "highlighter")]
|
#[cfg(feature = "highlighter")]
|
||||||
{
|
{
|
||||||
highlighter = None;
|
state.borrow_mut().highlighter = highlighter.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
produce(
|
produce(
|
||||||
|
state.borrow_mut(),
|
||||||
&mut lists,
|
&mut lists,
|
||||||
Item::CodeBlock(Text::new(spans.drain(..).collect())),
|
Item::CodeBlock(code.drain(..).collect()),
|
||||||
|
source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pulldown_cmark::TagEnd::MetadataBlock(_) => {
|
pulldown_cmark::TagEnd::MetadataBlock(_) => {
|
||||||
|
|
@ -430,18 +605,10 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
pulldown_cmark::Event::Text(text) if !metadata && !table => {
|
pulldown_cmark::Event::Text(text) if !metadata && !table => {
|
||||||
#[cfg(feature = "highlighter")]
|
#[cfg(feature = "highlighter")]
|
||||||
if let Some(highlighter) = &mut highlighter {
|
if let Some(highlighter) = &mut highlighter {
|
||||||
use text::Highlighter as _;
|
for line in text.lines() {
|
||||||
|
code.push(Text::new(
|
||||||
for (range, highlight) in
|
highlighter.highlight_line(line).to_vec(),
|
||||||
highlighter.highlight_line(text.as_ref())
|
));
|
||||||
{
|
|
||||||
let span = Span::Highlight {
|
|
||||||
text: text[range].to_owned(),
|
|
||||||
color: highlight.color(),
|
|
||||||
font: highlight.font(),
|
|
||||||
};
|
|
||||||
|
|
||||||
spans.push(span);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -518,6 +685,8 @@ pub struct Settings {
|
||||||
pub h6_size: Pixels,
|
pub h6_size: Pixels,
|
||||||
/// The text size used in code blocks.
|
/// The text size used in code blocks.
|
||||||
pub code_size: Pixels,
|
pub code_size: Pixels,
|
||||||
|
/// The spacing to be used between elements.
|
||||||
|
pub spacing: Pixels,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
|
|
@ -538,6 +707,7 @@ impl Settings {
|
||||||
h5_size: text_size,
|
h5_size: text_size,
|
||||||
h6_size: text_size,
|
h6_size: text_size,
|
||||||
code_size: text_size * 0.75,
|
code_size: text_size * 0.75,
|
||||||
|
spacing: text_size * 0.875,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -640,10 +810,9 @@ where
|
||||||
h5_size,
|
h5_size,
|
||||||
h6_size,
|
h6_size,
|
||||||
code_size,
|
code_size,
|
||||||
|
spacing,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
let spacing = text_size * 0.625;
|
|
||||||
|
|
||||||
let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
|
let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
|
||||||
Item::Heading(level, heading) => {
|
Item::Heading(level, heading) => {
|
||||||
container(rich_text(heading.spans(style)).size(match level {
|
container(rich_text(heading.spans(style)).size(match level {
|
||||||
|
|
@ -666,11 +835,21 @@ where
|
||||||
}
|
}
|
||||||
Item::List { start: None, items } => {
|
Item::List { start: None, items } => {
|
||||||
column(items.iter().map(|items| {
|
column(items.iter().map(|items| {
|
||||||
row![text("•").size(text_size), view(items, settings, style)]
|
row![
|
||||||
.spacing(spacing)
|
text("•").size(text_size),
|
||||||
.into()
|
view(
|
||||||
|
items,
|
||||||
|
Settings {
|
||||||
|
spacing: settings.spacing * 0.6,
|
||||||
|
..settings
|
||||||
|
},
|
||||||
|
style
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.spacing(spacing)
|
||||||
|
.into()
|
||||||
}))
|
}))
|
||||||
.spacing(spacing)
|
.spacing(spacing * 0.75)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
Item::List {
|
Item::List {
|
||||||
|
|
@ -679,20 +858,28 @@ where
|
||||||
} => column(items.iter().enumerate().map(|(i, items)| {
|
} => column(items.iter().enumerate().map(|(i, items)| {
|
||||||
row![
|
row![
|
||||||
text!("{}.", i as u64 + *start).size(text_size),
|
text!("{}.", i as u64 + *start).size(text_size),
|
||||||
view(items, settings, style)
|
view(
|
||||||
|
items,
|
||||||
|
Settings {
|
||||||
|
spacing: settings.spacing * 0.6,
|
||||||
|
..settings
|
||||||
|
},
|
||||||
|
style
|
||||||
|
)
|
||||||
]
|
]
|
||||||
.spacing(spacing)
|
.spacing(spacing)
|
||||||
.into()
|
.into()
|
||||||
}))
|
}))
|
||||||
.spacing(spacing)
|
.spacing(spacing * 0.75)
|
||||||
.into(),
|
.into(),
|
||||||
Item::CodeBlock(code) => container(
|
Item::CodeBlock(lines) => container(
|
||||||
scrollable(
|
scrollable(
|
||||||
container(
|
container(column(lines.iter().map(|line| {
|
||||||
rich_text(code.spans(style))
|
rich_text(line.spans(style))
|
||||||
.font(Font::MONOSPACE)
|
.font(Font::MONOSPACE)
|
||||||
.size(code_size),
|
.size(code_size)
|
||||||
)
|
.into()
|
||||||
|
})))
|
||||||
.padding(spacing.0 / 2.0),
|
.padding(spacing.0 / 2.0),
|
||||||
)
|
)
|
||||||
.direction(scrollable::Direction::Horizontal(
|
.direction(scrollable::Direction::Horizontal(
|
||||||
|
|
@ -707,7 +894,7 @@ where
|
||||||
.into(),
|
.into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Element::new(column(blocks).width(Length::Fill).spacing(text_size))
|
Element::new(column(blocks).spacing(spacing))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The theme catalog of Markdown items.
|
/// The theme catalog of Markdown items.
|
||||||
|
|
|
||||||
|
|
@ -144,8 +144,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the [`Id`] of the [`Scrollable`].
|
/// Sets the [`Id`] of the [`Scrollable`].
|
||||||
pub fn id(mut self, id: Id) -> Self {
|
pub fn id(mut self, id: impl Into<Id>) -> Self {
|
||||||
self.id = Some(id);
|
self.id = Some(id.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -788,13 +788,7 @@ where
|
||||||
(x, y)
|
(x, y)
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_vertical = match self.direction {
|
let movement = if !is_shift_pressed {
|
||||||
Direction::Vertical(_) => true,
|
|
||||||
Direction::Horizontal(_) => false,
|
|
||||||
Direction::Both { .. } => !is_shift_pressed,
|
|
||||||
};
|
|
||||||
|
|
||||||
let movement = if is_vertical {
|
|
||||||
Vector::new(x, y)
|
Vector::new(x, y)
|
||||||
} else {
|
} else {
|
||||||
Vector::new(y, x)
|
Vector::new(y, x)
|
||||||
|
|
@ -999,9 +993,9 @@ where
|
||||||
content_layout,
|
content_layout,
|
||||||
cursor,
|
cursor,
|
||||||
&Rectangle {
|
&Rectangle {
|
||||||
y: bounds.y + translation.y,
|
y: visible_bounds.y + translation.y,
|
||||||
x: bounds.x + translation.x,
|
x: visible_bounds.x + translation.x,
|
||||||
..bounds
|
..visible_bounds
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1103,9 +1097,9 @@ where
|
||||||
content_layout,
|
content_layout,
|
||||||
cursor,
|
cursor,
|
||||||
&Rectangle {
|
&Rectangle {
|
||||||
x: bounds.x + translation.x,
|
x: visible_bounds.x + translation.x,
|
||||||
y: bounds.y + translation.y,
|
y: visible_bounds.y + translation.y,
|
||||||
..bounds
|
..visible_bounds
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1228,25 +1222,36 @@ impl From<Id> for widget::Id {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for Id {
|
||||||
|
fn from(id: &'static str) -> Self {
|
||||||
|
Self::new(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`]
|
/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`]
|
||||||
/// to the provided [`RelativeOffset`].
|
/// to the provided [`RelativeOffset`].
|
||||||
pub fn snap_to<T>(id: Id, offset: RelativeOffset) -> Task<T> {
|
pub fn snap_to<T>(id: impl Into<Id>, offset: RelativeOffset) -> Task<T> {
|
||||||
task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset)))
|
task::effect(Action::widget(operation::scrollable::snap_to(
|
||||||
|
id.into().0,
|
||||||
|
offset,
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
|
/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
|
||||||
/// to the provided [`AbsoluteOffset`].
|
/// to the provided [`AbsoluteOffset`].
|
||||||
pub fn scroll_to<T>(id: Id, offset: AbsoluteOffset) -> Task<T> {
|
pub fn scroll_to<T>(id: impl Into<Id>, offset: AbsoluteOffset) -> Task<T> {
|
||||||
task::effect(Action::widget(operation::scrollable::scroll_to(
|
task::effect(Action::widget(operation::scrollable::scroll_to(
|
||||||
id.0, offset,
|
id.into().0,
|
||||||
|
offset,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
|
/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
|
||||||
/// by the provided [`AbsoluteOffset`].
|
/// by the provided [`AbsoluteOffset`].
|
||||||
pub fn scroll_by<T>(id: Id, offset: AbsoluteOffset) -> Task<T> {
|
pub fn scroll_by<T>(id: impl Into<Id>, offset: AbsoluteOffset) -> Task<T> {
|
||||||
task::effect(Action::widget(operation::scrollable::scroll_by(
|
task::effect(Action::widget(operation::scrollable::scroll_by(
|
||||||
id.0, offset,
|
id.into().0,
|
||||||
|
offset,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,8 @@ where
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
let is_over = cursor.is_over(layout.bounds());
|
let is_over = cursor.is_over(layout.bounds());
|
||||||
|
let is_mouse_movement =
|
||||||
|
matches!(event, Event::Mouse(mouse::Event::CursorMoved { .. }));
|
||||||
|
|
||||||
for ((child, state), layout) in self
|
for ((child, state), layout) in self
|
||||||
.children
|
.children
|
||||||
|
|
@ -235,7 +237,10 @@ where
|
||||||
viewport,
|
viewport,
|
||||||
);
|
);
|
||||||
|
|
||||||
if is_over && cursor != mouse::Cursor::Unavailable {
|
if is_over
|
||||||
|
&& !is_mouse_movement
|
||||||
|
&& cursor != mouse::Cursor::Unavailable
|
||||||
|
{
|
||||||
let interaction = child.as_widget().mouse_interaction(
|
let interaction = child.as_widget().mouse_interaction(
|
||||||
state, layout, cursor, viewport, renderer,
|
state, layout, cursor, viewport, renderer,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ where
|
||||||
align_y: alignment::Vertical,
|
align_y: alignment::Vertical,
|
||||||
wrapping: Wrapping,
|
wrapping: Wrapping,
|
||||||
class: Theme::Class<'a>,
|
class: Theme::Class<'a>,
|
||||||
|
hovered_link: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Link, Theme, Renderer> Rich<'a, Link, Theme, Renderer>
|
impl<'a, Link, Theme, Renderer> Rich<'a, Link, Theme, Renderer>
|
||||||
|
|
@ -52,6 +53,7 @@ where
|
||||||
align_y: alignment::Vertical::Top,
|
align_y: alignment::Vertical::Top,
|
||||||
wrapping: Wrapping::default(),
|
wrapping: Wrapping::default(),
|
||||||
class: Theme::default(),
|
class: Theme::default(),
|
||||||
|
hovered_link: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,22 +238,21 @@ where
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
defaults: &renderer::Style,
|
defaults: &renderer::Style,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
_cursor: mouse::Cursor,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
|
if !layout.bounds().intersects(viewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let state = tree
|
let state = tree
|
||||||
.state
|
.state
|
||||||
.downcast_ref::<State<Link, Renderer::Paragraph>>();
|
.downcast_ref::<State<Link, Renderer::Paragraph>>();
|
||||||
|
|
||||||
let style = theme.style(&self.class);
|
let style = theme.style(&self.class);
|
||||||
|
|
||||||
let hovered_span = cursor
|
|
||||||
.position_in(layout.bounds())
|
|
||||||
.and_then(|position| state.paragraph.hit_span(position));
|
|
||||||
|
|
||||||
for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
|
for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
|
||||||
let is_hovered_link =
|
let is_hovered_link = Some(index) == self.hovered_link;
|
||||||
span.link.is_some() && Some(index) == hovered_span;
|
|
||||||
|
|
||||||
if span.highlight.is_some()
|
if span.highlight.is_some()
|
||||||
|| span.underline
|
|| span.underline
|
||||||
|
|
@ -365,25 +366,38 @@ where
|
||||||
shell: &mut Shell<'_, Link>,
|
shell: &mut Shell<'_, Link>,
|
||||||
_viewport: &Rectangle,
|
_viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
|
let was_hovered = self.hovered_link.is_some();
|
||||||
|
|
||||||
|
if let Some(position) = cursor.position_in(layout.bounds()) {
|
||||||
|
let state = tree
|
||||||
|
.state
|
||||||
|
.downcast_ref::<State<Link, Renderer::Paragraph>>();
|
||||||
|
|
||||||
|
self.hovered_link =
|
||||||
|
state.paragraph.hit_span(position).and_then(|span| {
|
||||||
|
if self.spans.as_ref().as_ref().get(span)?.link.is_some() {
|
||||||
|
Some(span)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.hovered_link = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if was_hovered != self.hovered_link.is_some() {
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||||
if let Some(position) = cursor.position_in(layout.bounds()) {
|
let state = tree
|
||||||
let state = tree
|
.state
|
||||||
.state
|
.downcast_mut::<State<Link, Renderer::Paragraph>>();
|
||||||
.downcast_mut::<State<Link, Renderer::Paragraph>>();
|
|
||||||
|
|
||||||
if let Some(span) = state.paragraph.hit_span(position) {
|
if self.hovered_link.is_some() {
|
||||||
if self
|
state.span_pressed = self.hovered_link;
|
||||||
.spans
|
shell.capture_event();
|
||||||
.as_ref()
|
|
||||||
.as_ref()
|
|
||||||
.get(span)
|
|
||||||
.is_some_and(|span| span.link.is_some())
|
|
||||||
{
|
|
||||||
state.span_pressed = Some(span);
|
|
||||||
shell.capture_event();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||||
|
|
@ -391,27 +405,22 @@ where
|
||||||
.state
|
.state
|
||||||
.downcast_mut::<State<Link, Renderer::Paragraph>>();
|
.downcast_mut::<State<Link, Renderer::Paragraph>>();
|
||||||
|
|
||||||
if let Some(span_pressed) = state.span_pressed {
|
match state.span_pressed {
|
||||||
state.span_pressed = None;
|
Some(span) if Some(span) == self.hovered_link => {
|
||||||
|
if let Some(link) = self
|
||||||
if let Some(position) = cursor.position_in(layout.bounds())
|
.spans
|
||||||
{
|
.as_ref()
|
||||||
match state.paragraph.hit_span(position) {
|
.as_ref()
|
||||||
Some(span) if span == span_pressed => {
|
.get(span)
|
||||||
if let Some(link) = self
|
.and_then(|span| span.link.clone())
|
||||||
.spans
|
{
|
||||||
.as_ref()
|
shell.publish(link);
|
||||||
.as_ref()
|
|
||||||
.get(span)
|
|
||||||
.and_then(|span| span.link.clone())
|
|
||||||
{
|
|
||||||
shell.publish(link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.span_pressed = None;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -419,29 +428,17 @@ where
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn mouse_interaction(
|
||||||
&self,
|
&self,
|
||||||
tree: &Tree,
|
_tree: &Tree,
|
||||||
layout: Layout<'_>,
|
_layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
_cursor: mouse::Cursor,
|
||||||
_viewport: &Rectangle,
|
_viewport: &Rectangle,
|
||||||
_renderer: &Renderer,
|
_renderer: &Renderer,
|
||||||
) -> mouse::Interaction {
|
) -> mouse::Interaction {
|
||||||
if let Some(position) = cursor.position_in(layout.bounds()) {
|
if self.hovered_link.is_some() {
|
||||||
let state = tree
|
mouse::Interaction::Pointer
|
||||||
.state
|
} else {
|
||||||
.downcast_ref::<State<Link, Renderer::Paragraph>>();
|
mouse::Interaction::None
|
||||||
|
|
||||||
if let Some(span) = state
|
|
||||||
.paragraph
|
|
||||||
.hit_span(position)
|
|
||||||
.and_then(|span| self.spans.as_ref().as_ref().get(span))
|
|
||||||
{
|
|
||||||
if span.link.is_some() {
|
|
||||||
return mouse::Interaction::Pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mouse::Interaction::None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,8 @@ pub struct TextEditor<
|
||||||
line_height: LineHeight,
|
line_height: LineHeight,
|
||||||
width: Length,
|
width: Length,
|
||||||
height: Length,
|
height: Length,
|
||||||
|
min_height: f32,
|
||||||
|
max_height: f32,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
wrapping: Wrapping,
|
wrapping: Wrapping,
|
||||||
class: Theme::Class<'a>,
|
class: Theme::Class<'a>,
|
||||||
|
|
@ -139,6 +141,8 @@ where
|
||||||
line_height: LineHeight::default(),
|
line_height: LineHeight::default(),
|
||||||
width: Length::Fill,
|
width: Length::Fill,
|
||||||
height: Length::Shrink,
|
height: Length::Shrink,
|
||||||
|
min_height: 0.0,
|
||||||
|
max_height: f32::INFINITY,
|
||||||
padding: Padding::new(5.0),
|
padding: Padding::new(5.0),
|
||||||
wrapping: Wrapping::default(),
|
wrapping: Wrapping::default(),
|
||||||
class: Theme::default(),
|
class: Theme::default(),
|
||||||
|
|
@ -169,15 +173,27 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the width of the [`TextEditor`].
|
||||||
|
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
|
||||||
|
self.width = Length::from(width.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the height of the [`TextEditor`].
|
/// Sets the height of the [`TextEditor`].
|
||||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
self.height = height.into();
|
self.height = height.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the width of the [`TextEditor`].
|
/// Sets the minimum height of the [`TextEditor`].
|
||||||
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
|
pub fn min_height(mut self, min_height: impl Into<Pixels>) -> Self {
|
||||||
self.width = Length::from(width.into());
|
self.min_height = min_height.into().0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum height of the [`TextEditor`].
|
||||||
|
pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
|
||||||
|
self.max_height = max_height.into().0;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,6 +281,8 @@ where
|
||||||
line_height: self.line_height,
|
line_height: self.line_height,
|
||||||
width: self.width,
|
width: self.width,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
|
min_height: self.min_height,
|
||||||
|
max_height: self.max_height,
|
||||||
padding: self.padding,
|
padding: self.padding,
|
||||||
wrapping: self.wrapping,
|
wrapping: self.wrapping,
|
||||||
class: self.class,
|
class: self.class,
|
||||||
|
|
@ -549,7 +567,11 @@ where
|
||||||
state.highlighter_settings = self.highlighter_settings.clone();
|
state.highlighter_settings = self.highlighter_settings.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
let limits = limits.width(self.width).height(self.height);
|
let limits = limits
|
||||||
|
.width(self.width)
|
||||||
|
.height(self.height)
|
||||||
|
.min_height(self.min_height)
|
||||||
|
.max_height(self.max_height);
|
||||||
|
|
||||||
internal.editor.update(
|
internal.editor.update(
|
||||||
limits.shrink(self.padding).max(),
|
limits.shrink(self.padding).max(),
|
||||||
|
|
@ -768,6 +790,10 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !matches!(binding, Binding::Unfocus) {
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
|
||||||
apply_binding(
|
apply_binding(
|
||||||
binding,
|
binding,
|
||||||
self.content,
|
self.content,
|
||||||
|
|
@ -780,8 +806,6 @@ where
|
||||||
if let Some(focus) = &mut state.focus {
|
if let Some(focus) = &mut state.focus {
|
||||||
focus.updated_at = Instant::now();
|
focus.updated_at = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
shell.capture_event();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue