aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPolesznyák Márk <contact@pml68.dev>2025-10-20 01:31:15 +0200
committerPolesznyák Márk <contact@pml68.dev>2025-10-20 01:31:15 +0200
commit2a6e3a98a2452045622a4746558ec46cdfa8fc1d (patch)
tree195dd69196b6828e9ca9ad8d4267518437c361fc
parentchore: add a to-do list (diff)
downloadiced_selection-2a6e3a98a2452045622a4746558ec46cdfa8fc1d.tar.gz
feat(wip): add markdown support
-rw-r--r--Cargo.toml6
-rw-r--r--src/lib.rs2
-rw-r--r--src/markdown.rs255
3 files changed, 261 insertions, 2 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 0576705..849c30e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,11 +14,13 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
rust-version = "1.88"
[dependencies]
-iced_widget = { git = "https://github.com/iced-rs/iced" }
+iced_widget = { git = "https://github.com/iced-rs/iced", features = [
+ "markdown",
+] }
unicode-segmentation = "1.0"
[dev-dependencies]
-iced = { git = "https://github.com/iced-rs/iced" }
+iced = { git = "https://github.com/iced-rs/iced", features = ["markdown"] }
open = "5.3"
[package.metadata.docs.rs]
diff --git a/src/lib.rs b/src/lib.rs
index ce5a124..a4c6a44 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,11 +2,13 @@
//!
//! [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html
+pub mod markdown;
pub mod selection;
pub mod text;
use iced_widget::core;
use iced_widget::graphics::text::Paragraph;
+pub use markdown::view as markdown;
#[doc(no_inline)]
pub use text::Text;
diff --git a/src/markdown.rs b/src/markdown.rs
new file mode 100644
index 0000000..c54271a
--- /dev/null
+++ b/src/markdown.rs
@@ -0,0 +1,255 @@
+//! A custom markdown viewer and its corresponding functions.
+//!
+//! To be used with [`view_with`]
+//!
+//! [`view_with`]: iced_widget::markdown::view_with
+use iced_widget::graphics::text::Paragraph;
+use iced_widget::markdown::{
+ Catalog, HeadingLevel, Item, Settings, Text, Url, Viewer, view_with,
+};
+use iced_widget::{column, container, row, scrollable};
+
+use crate::core::Font;
+use crate::core::alignment;
+use crate::core::padding;
+use crate::core::{self, Element, Length, Pixels};
+use crate::{rich_text, text};
+
+/// Display a bunch of markdown items.
+pub fn view<'a, Theme, Renderer>(
+ items: impl IntoIterator<Item = &'a Item>,
+ settings: impl Into<Settings>,
+) -> Element<'a, Url, Theme, Renderer>
+where
+ Theme: Catalog + text::Catalog + 'a,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
+{
+ view_with(items, settings, &SelectableViewer)
+}
+/// Displays a heading using the default look.
+pub fn heading<'a, Message, Theme, Renderer>(
+ settings: Settings,
+ level: &'a HeadingLevel,
+ text: &'a Text,
+ index: usize,
+ on_link_click: impl Fn(Url) -> Message + 'a,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + text::Catalog + 'a,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
+{
+ let Settings {
+ h1_size,
+ h2_size,
+ h3_size,
+ h4_size,
+ h5_size,
+ h6_size,
+ text_size,
+ ..
+ } = settings;
+
+ container(
+ rich_text(text.spans(settings.style))
+ .on_link_click(on_link_click)
+ .size(match level {
+ HeadingLevel::H1 => h1_size,
+ HeadingLevel::H2 => h2_size,
+ HeadingLevel::H3 => h3_size,
+ HeadingLevel::H4 => h4_size,
+ HeadingLevel::H5 => h5_size,
+ HeadingLevel::H6 => h6_size,
+ }),
+ )
+ .padding(padding::top(if index > 0 {
+ text_size / 2.0
+ } else {
+ Pixels::ZERO
+ }))
+ .into()
+}
+
+/// Displays a paragraph using the default look.
+pub fn paragraph<'a, Message, Theme, Renderer>(
+ settings: Settings,
+ text: &Text,
+ on_link_click: impl Fn(Url) -> Message + 'a,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + text::Catalog + 'a,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
+{
+ rich_text(text.spans(settings.style))
+ .size(settings.text_size)
+ .on_link_click(on_link_click)
+ .into()
+}
+
+/// Displays an unordered list using the default look and
+/// calling the [`Viewer`] for each bullet point item.
+pub fn unordered_list<'a, Message, Theme, Renderer>(
+ viewer: &impl Viewer<'a, Message, Theme, Renderer>,
+ settings: Settings,
+ items: &'a [Vec<Item>],
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + text::Catalog + 'a,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
+{
+ column(items.iter().map(|items| {
+ row![
+ text("•").size(settings.text_size),
+ view_with(
+ items,
+ Settings {
+ spacing: settings.spacing * 0.6,
+ ..settings
+ },
+ viewer,
+ )
+ ]
+ .spacing(settings.spacing)
+ .into()
+ }))
+ .spacing(settings.spacing * 0.75)
+ .padding([0.0, settings.spacing.0])
+ .into()
+}
+
+/// Displays an ordered list using the default look and
+/// calling the [`Viewer`] for each numbered item.
+pub fn ordered_list<'a, Message, Theme, Renderer>(
+ viewer: &impl Viewer<'a, Message, Theme, Renderer>,
+ settings: Settings,
+ start: u64,
+ items: &'a [Vec<Item>],
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + text::Catalog + 'a,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
+{
+ let digits = ((start + items.len() as u64).max(1) as f32).log10().ceil();
+
+ column(items.iter().enumerate().map(|(i, items)| {
+ row![
+ text!("{}.", i as u64 + start)
+ .size(settings.text_size)
+ .align_x(alignment::Horizontal::Right)
+ .width(settings.text_size * ((digits / 2.0).ceil() + 1.0)),
+ view_with(
+ items,
+ Settings {
+ spacing: settings.spacing * 0.6,
+ ..settings
+ },
+ viewer,
+ )
+ ]
+ .spacing(settings.spacing)
+ .into()
+ }))
+ .spacing(settings.spacing * 0.75)
+ .into()
+}
+
+/// Displays a code block using the default look.
+pub fn code_block<'a, Message, Theme, Renderer>(
+ settings: Settings,
+ lines: &'a [Text],
+ on_link_click: impl Fn(Url) -> Message + Clone + 'a,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + text::Catalog + 'a,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
+{
+ container(
+ scrollable(
+ container(column(lines.iter().map(|line| {
+ rich_text(line.spans(settings.style))
+ .on_link_click(on_link_click.clone())
+ .font(Font::MONOSPACE)
+ .size(settings.code_size)
+ .into()
+ })))
+ .padding(settings.code_size),
+ )
+ .direction(scrollable::Direction::Horizontal(
+ scrollable::Scrollbar::default()
+ .width(settings.code_size / 2)
+ .scroller_width(settings.code_size / 2),
+ )),
+ )
+ .width(Length::Fill)
+ .padding(settings.code_size / 4)
+ .class(Theme::code_block())
+ .into()
+}
+
+#[derive(Debug, Clone, Copy)]
+struct SelectableViewer;
+
+impl<'a, Theme, Renderer> Viewer<'a, Url, Theme, Renderer> for SelectableViewer
+where
+ Theme: Catalog + text::Catalog + 'a,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
+{
+ fn on_link_click(url: Url) -> Url {
+ url
+ }
+
+ fn heading(
+ &self,
+ settings: Settings,
+ level: &'a HeadingLevel,
+ text: &'a Text,
+ index: usize,
+ ) -> Element<'a, Url, Theme, Renderer> {
+ heading::<'a, Url, Theme, Renderer>(
+ settings,
+ level,
+ text,
+ index,
+ Self::on_link_click,
+ )
+ }
+
+ fn paragraph(
+ &self,
+ settings: Settings,
+ text: &Text,
+ ) -> Element<'a, Url, Theme, Renderer> {
+ paragraph(settings, text, Self::on_link_click)
+ }
+
+ fn unordered_list(
+ &self,
+ settings: Settings,
+ items: &'a [Vec<Item>],
+ ) -> Element<'a, Url, Theme, Renderer> {
+ unordered_list(self, settings, items)
+ }
+
+ fn ordered_list(
+ &self,
+ settings: Settings,
+ start: u64,
+ items: &'a [Vec<Item>],
+ ) -> Element<'a, Url, Theme, Renderer> {
+ ordered_list(self, settings, start, items)
+ }
+
+ fn code_block(
+ &self,
+ settings: Settings,
+ _language: Option<&'a str>,
+ _code: &'a str,
+ lines: &'a [Text],
+ ) -> Element<'a, Url, Theme, Renderer> {
+ code_block(settings, lines, Self::on_link_click)
+ }
+}