Add Copy action to code blocks in markdown example

This commit is contained in:
Héctor Ramón Jiménez 2025-02-04 20:58:06 +01:00
parent 387abafa3a
commit e8020f3eaf
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
9 changed files with 93 additions and 12 deletions

8
Cargo.lock generated
View file

@ -770,9 +770,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.11" version = "1.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -877,9 +877,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.27" version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
] ]

View file

@ -79,3 +79,11 @@ impl std::ops::Div<f32> for Pixels {
Pixels(self.0 / rhs) Pixels(self.0 / rhs)
} }
} }
impl std::ops::Div<u32> for Pixels {
type Output = Pixels;
fn div(self, rhs: u32) -> Self {
Pixels(self.0 / rhs as f32)
}
}

View file

@ -16,3 +16,8 @@ image.workspace = true
tokio.workspace = true tokio.workspace = true
open = "5.3" open = "5.3"
# Disabled to keep amount of build dependencies low
# This can be re-enabled on demand
# [build-dependencies]
# iced_fontello = "0.13"

View file

@ -0,0 +1,5 @@
pub fn main() {
// println!("cargo::rerun-if-changed=fonts/markdown-icons.toml");
// iced_fontello::build("fonts/markdown-icons.toml")
// .expect("Build icons font");
}

View file

@ -0,0 +1,4 @@
module = "icon"
[glyphs]
copy = "fontawesome-docs"

Binary file not shown.

View file

@ -0,0 +1,15 @@
// Generated automatically by iced_fontello at build time.
// Do not edit manually. Source: ../fonts/markdown-icons.toml
// dcd2f0c969d603e2ee9237a4b70fa86b1a6e84d86f4689046d8fdd10440b06b9
use iced::widget::{text, Text};
use iced::Font;
pub const FONT: &[u8] = include_bytes!("../fonts/markdown-icons.ttf");
pub fn copy<'a>() -> Text<'a> {
icon("\u{F0C5}")
}
fn icon(codepoint: &str) -> Text<'_> {
text(codepoint).font(Font::with_name("markdown-icons"))
}

View file

@ -1,10 +1,13 @@
mod icon;
use iced::animation; use iced::animation;
use iced::clipboard;
use iced::highlighter; use iced::highlighter;
use iced::task; use iced::task;
use iced::time::{self, milliseconds, Instant}; use iced::time::{self, milliseconds, Instant};
use iced::widget::{ use iced::widget::{
self, center_x, horizontal_space, hover, image, markdown, pop, right, row, self, button, center_x, horizontal_space, hover, image, markdown, pop,
scrollable, text_editor, toggler, right, row, scrollable, text_editor, toggler,
}; };
use iced::window; use iced::window;
use iced::{Animation, Element, Fill, Font, Subscription, Task, Theme}; use iced::{Animation, Element, Fill, Font, Subscription, Task, Theme};
@ -15,6 +18,7 @@ use std::sync::Arc;
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)
.font(icon::FONT)
.subscription(Markdown::subscription) .subscription(Markdown::subscription)
.theme(Markdown::theme) .theme(Markdown::theme)
.run_with(Markdown::new) .run_with(Markdown::new)
@ -49,6 +53,7 @@ enum Image {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { enum Message {
Edit(text_editor::Action), Edit(text_editor::Action),
Copy(String),
LinkClicked(markdown::Url), LinkClicked(markdown::Url),
ImageShown(markdown::Url), ImageShown(markdown::Url),
ImageDownloaded(markdown::Url, Result<image::Handle, Error>), ImageDownloaded(markdown::Url, Result<image::Handle, Error>),
@ -91,6 +96,7 @@ impl Markdown {
Task::none() Task::none()
} }
Message::Copy(content) => clipboard::write(content),
Message::LinkClicked(link) => { Message::LinkClicked(link) => {
let _ = open::that_in_background(link.to_string()); let _ = open::that_in_background(link.to_string());
@ -141,6 +147,8 @@ impl Markdown {
} }
Message::ToggleStream(enable_stream) => { Message::ToggleStream(enable_stream) => {
if enable_stream { if enable_stream {
self.content = markdown::Content::new();
self.mode = Mode::Stream { self.mode = Mode::Stream {
pending: self.raw.text(), pending: self.raw.text(),
}; };
@ -282,6 +290,26 @@ impl<'a> markdown::Viewer<'a, Message> for CustomViewer<'a> {
.into() .into()
} }
} }
fn code_block(
&self,
settings: markdown::Settings,
code: &'a str,
lines: &'a [markdown::Text],
) -> Element<'a, Message> {
let code_block =
markdown::code_block(settings, code, lines, Message::LinkClicked);
hover(
code_block,
right(
button(icon::copy().size(12))
.padding(settings.spacing / 2)
.on_press_with(|| Message::Copy(code.to_owned()))
.style(button::text),
),
)
}
} }
async fn download_image(url: markdown::Url) -> Result<image::Handle, Error> { async fn download_image(url: markdown::Url) -> Result<image::Handle, Error> {

View file

@ -55,6 +55,7 @@ use crate::{column, container, rich_text, row, scrollable, span, text};
use std::borrow::BorrowMut; use std::borrow::BorrowMut;
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::mem;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
@ -182,7 +183,12 @@ 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(Vec<Text>), CodeBlock {
/// The raw code of the code block.
code: String,
/// The styled lines of text in the code block.
lines: 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.
@ -457,7 +463,8 @@ fn parse_with<'a>(
let broken_links = Rc::new(RefCell::new(HashSet::new())); let broken_links = Rc::new(RefCell::new(HashSet::new()));
let mut spans = Vec::new(); let mut spans = Vec::new();
let mut code = Vec::new(); let mut code = String::new();
let mut code_lines = 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;
@ -726,7 +733,10 @@ fn parse_with<'a>(
produce( produce(
state.borrow_mut(), state.borrow_mut(),
&mut stack, &mut stack,
Item::CodeBlock(code.drain(..).collect()), Item::CodeBlock {
code: mem::take(&mut code),
lines: code_lines.drain(..).collect(),
},
source, source,
) )
} }
@ -743,8 +753,10 @@ fn parse_with<'a>(
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 {
code.push_str(&text);
for line in text.lines() { for line in text.lines() {
code.push(Text::new( code_lines.push(Text::new(
highlighter.highlight_line(line).to_vec(), highlighter.highlight_line(line).to_vec(),
)); ));
} }
@ -1017,7 +1029,9 @@ where
viewer.heading(settings, level, text, index) viewer.heading(settings, level, text, index)
} }
Item::Paragraph(text) => viewer.paragraph(settings, text), Item::Paragraph(text) => viewer.paragraph(settings, text),
Item::CodeBlock(lines) => viewer.code_block(settings, lines), Item::CodeBlock { code, lines } => {
viewer.code_block(settings, code, lines)
}
Item::List { start: None, items } => { Item::List { start: None, items } => {
viewer.unordered_list(settings, items) viewer.unordered_list(settings, items)
} }
@ -1157,6 +1171,7 @@ where
/// Displays a code block using the default look. /// Displays a code block using the default look.
pub fn code_block<'a, Message, Theme, Renderer>( pub fn code_block<'a, Message, Theme, Renderer>(
settings: Settings, settings: Settings,
_code: &'a str,
lines: &'a [Text], lines: &'a [Text],
on_link_clicked: impl Fn(Url) -> Message + Clone + 'a, on_link_clicked: impl Fn(Url) -> Message + Clone + 'a,
) -> Element<'a, Message, Theme, Renderer> ) -> Element<'a, Message, Theme, Renderer>
@ -1251,9 +1266,10 @@ where
fn code_block( fn code_block(
&self, &self,
settings: Settings, settings: Settings,
code: &'a str,
lines: &'a [Text], lines: &'a [Text],
) -> Element<'a, Message, Theme, Renderer> { ) -> Element<'a, Message, Theme, Renderer> {
code_block(settings, lines, Self::on_link_clicked) code_block(settings, code, lines, Self::on_link_clicked)
} }
/// Displays an unordered list. /// Displays an unordered list.