feat: add code block and update the open link dialogs with the code block (#924)

This commit is contained in:
LynithDev 2026-10-03 15:17:52 +02:00 • committed by GitHub
parent cfcf7a7669
commit c43272683b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 169 additions and 87 deletions

View file

@ -0,0 +1,5 @@
---
default: minor
---
# Code blocks are selectable, and the open link confirmation has a copy button

View file

@ -478,3 +478,29 @@ pub fn open_folder_button(folder: std::path::PathBuf) -> Button {
})
.child(Icon::new(IconType::Folder).size(16.))
}
pub fn copy_button(text: String, copied_body: &'static str, dispatch: crate::Actions) -> Button {
Button::new()
.secondary()
.on_press(move |_| {
if let Err(err) = freya::text_edit::Clipboard::set(text.clone()) {
tracing::warn!("clipboard copy failed: {err:?}");
dispatch
.notify("Copy failed")
.body("Could not copy to the clipboard.")
.error()
.toast_only()
.send();
} else {
dispatch
.notify("Copied to clipboard")
.body(copied_body)
.info()
.icon(IconType::ClipboardCheck)
.toast_only()
.send();
}
})
.child(Icon::new(IconType::Copy01).size(14.))
.text("Copy")
}

View file

@ -0,0 +1,107 @@
use std::borrow::Cow;
use freya::prelude::*;
use crate::components::ScrollArea;
use crate::theme::{self, colors};
use crate::ui::border_all_color;
#[derive(PartialEq)]
pub struct CodeBlock {
code: String,
color: Color,
background: Color,
font_family: Cow<'static, str>,
font_size: f32,
max_lines: Option<usize>,
height: Option<f32>,
key: DiffKey,
}
impl KeyExt for CodeBlock {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}
impl CodeBlock {
pub fn new(code: impl Into<String>) -> Self {
Self {
code: code.into(),
color: colors::fg_primary(),
background: colors::component_bg(),
font_family: Cow::Borrowed(theme::MONO_FONT),
font_size: 12.,
max_lines: None,
height: None,
key: DiffKey::None,
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn background(mut self, background: Color) -> Self {
self.background = background;
self
}
pub fn font_family(mut self, font_family: Cow<'static, str>) -> Self {
self.font_family = font_family;
self
}
pub fn font_size(mut self, font_size: f32) -> Self {
self.font_size = font_size;
self
}
pub fn max_lines(mut self, max_lines: usize) -> Self {
self.max_lines = Some(max_lines);
self
}
/// Fixes the block's height and scrolls the code inside it
pub fn height(mut self, height: f32) -> Self {
self.height = Some(height);
self
}
}
impl Component for CodeBlock {
fn render(&self) -> impl IntoElement {
let text = SelectableText::new()
.span(self.code.clone())
.font_family(self.font_family.clone())
.font_size(self.font_size)
.max_lines(self.max_lines)
.width(Size::fill())
.color(self.color);
let block = rect()
.width(Size::fill())
.padding(Gaps::new_all(12.))
.corner_radius(CornerRadius::new_all(8.))
.background(self.background)
.border(border_all_color(1., colors::component_border()));
match self.height {
Some(height) => block
.height(Size::px(height))
.overflow(Overflow::Clip)
.child(
ScrollArea::new()
.width(Size::fill())
.height(Size::fill())
.child(text),
),
None => block.child(text),
}
}
fn render_key(&self) -> DiffKey {
self.key.clone().or(self.default_key())
}
}

View file

@ -1,7 +1,9 @@
use freya::prelude::*;
use crate::components::{Button, Icon, IconType, OVERLAY_BASE_LEVEL, OverlayPopup};
use crate::hooks::use_link_confirm;
use crate::components::{
Button, CodeBlock, Icon, IconType, OVERLAY_BASE_LEVEL, OverlayPopup, copy_button,
};
use crate::hooks::{use_dispatch, use_link_confirm};
use crate::theme::colors;
use crate::ui::border_all_color;
@ -13,6 +15,7 @@ pub struct ConfirmLinkOverlay;
impl Component for ConfirmLinkOverlay {
fn render(&self) -> impl IntoElement {
let mut pending = use_link_confirm();
let dispatch = use_dispatch();
let Some(url) = pending.read().clone() else {
return rect().into_element();
};
@ -73,20 +76,9 @@ impl Component for ConfirmLinkOverlay {
.color(colors::fg_secondary()),
)
.child(
rect()
.width(Size::fill())
.padding(Gaps::new_all(10.))
.corner_radius(CornerRadius::new_all(8.))
.background(colors::component_bg())
.border(border_all_color(1., colors::component_border()))
.child(
label()
.text(url.clone())
.font_size(12.)
.max_lines(4)
.width(Size::fill())
.color(colors::code_info()),
),
CodeBlock::new(url.clone())
.color(colors::code_info())
.max_lines(4),
)
.child(
rect()
@ -94,6 +86,11 @@ impl Component for ConfirmLinkOverlay {
.width(Size::fill())
.main_align(Alignment::End)
.spacing(8.)
.child(copy_button(
url.clone(),
"Link copied to your clipboard.",
dispatch,
))
.child(
Button::new()
.secondary()

View file

@ -3,6 +3,7 @@ use freya::prelude::*;
use super::MarkdownStyle;
use super::image::MarkdownImage;
use super::parse::{Block, Inline, List, TextSpan};
use crate::components::CodeBlock;
use crate::hooks::LinkConfirmState;
pub fn render_block(block: &Block, key: usize, style: &MarkdownStyle) -> Element {
@ -19,19 +20,12 @@ pub fn render_block(block: &Block, key: usize, style: &MarkdownStyle) -> Element
Block::Paragraph { content } => render_content(content, style.paragraph_size, style)
.key(key)
.into(),
Block::Code { code } => rect()
.key(key)
.width(Size::fill())
Block::Code { code } => CodeBlock::new(code.clone())
.color(style.color_code)
.background(style.background_code)
.corner_radius(CornerRadius::new_all(6.))
.padding(Gaps::new_all(12.))
.child(
label()
.text(code.clone())
.font_family(style.code_font_family.clone())
.font_size(style.code_font_size)
.color(style.color_code),
)
.font_family(style.code_font_family.clone())
.font_size(style.code_font_size)
.key(key)
.into(),
Block::List(list) => render_list(list, style).key(key).into(),
Block::Image { url, alt } => rect()

View file

@ -7,6 +7,7 @@ mod button;
mod charts;
mod checkbox;
mod cluster_update_popup;
mod code_block;
mod context_menu;
mod control_center;
mod date_range_picker;
@ -62,10 +63,11 @@ pub use active_cluster_panel::ActiveClusterPanel;
pub use asset_image::AssetImage;
pub use avatar::Avatar;
pub use blocked_downloads_popup::BlockedDownloadsPopup;
pub use button::{Button, ButtonSize, ButtonVariant, link_button, open_folder_button};
pub use button::{Button, ButtonSize, ButtonVariant, copy_button, link_button, open_folder_button};
pub use charts::{BarChart, PieChart, ValueUnit, slice_color};
pub use checkbox::checkbox_labeled;
pub use cluster_update_popup::ClusterUpdatePopup;
pub use code_block::CodeBlock;
pub use context_menu::ContextMenu;
pub use control_center::ControlCenter;
pub use date_range_picker::{DateRange, DateRangePicker};

View file

@ -10,11 +10,10 @@ use skia_safe::sampling_options::CubicResampler;
use crate::Route;
use crate::components::Button;
use crate::components::{
ART_PREVIEW_EDGE, AppNavbar, DynamicArt, FileDropOverlay, Icon, IconType, OverlayPopup,
ScrollArea, accept_drop,
ART_PREVIEW_EDGE, AppNavbar, CodeBlock, DynamicArt, FileDropOverlay, Icon, IconType,
OverlayPopup, accept_drop, copy_button,
};
use crate::layout::AnimatedAppOutlet;
use crate::theme;
use crate::use_settings_snapshot;
use oneclient_core::clusters::Cluster;
use oneclient_core::images::BACKGROUND_IMAGE_EDGE;
@ -121,20 +120,6 @@ struct LaunchErrorDialog {
impl Component for LaunchErrorDialog {
fn render(&self) -> impl IntoElement {
let dispatch = crate::hooks::use_dispatch();
let lines: Vec<Element> = self
.message
.lines()
.map(|line| {
label()
.text(line.to_string())
.font_family(theme::MONO_FONT)
.font_size(12.)
.width(Size::fill())
.color(colors::fg_primary())
.into_element()
})
.collect();
let close = dispatch.clone();
let outside_close = dispatch.clone();
let copy_dispatch = dispatch.clone();
@ -181,19 +166,9 @@ impl Component for LaunchErrorDialog {
.color(colors::fg_secondary()),
)
.child(
rect()
.width(Size::fill())
.height(Size::px(260.))
CodeBlock::new(self.message.clone())
.background(CODE_BG)
.corner_radius(CornerRadius::new_all(8.))
.padding(Gaps::new_all(12.))
.overflow(Overflow::Clip)
.child(
ScrollArea::new()
.width(Size::fill())
.height(Size::fill())
.children(lines),
),
.height(260.),
)
.child(
rect()
@ -201,7 +176,11 @@ impl Component for LaunchErrorDialog {
.width(Size::fill())
.main_align(Alignment::End)
.spacing(10.)
.child(copy_error_button(&self.message, copy_dispatch))
.child(copy_button(
self.message.clone(),
"Error message copied to your clipboard.",
copy_dispatch,
))
.child(
Button::new()
.primary()
@ -214,34 +193,6 @@ impl Component for LaunchErrorDialog {
}
}
fn copy_error_button(message: &str, dispatch: crate::Actions) -> impl IntoElement {
let message = message.to_string();
Button::new()
.secondary()
.on_press(move |_| {
if let Err(err) = freya::text_edit::Clipboard::set(message.clone()) {
tracing::warn!("clipboard copy failed: {err:?}");
dispatch
.notify("Copy failed")
.body("Could not copy the error to the clipboard.")
.error()
.toast_only()
.send();
} else {
dispatch
.notify("Copied to clipboard")
.body("Error message copied to your clipboard.")
.info()
.icon(IconType::ClipboardCheck)
.toast_only()
.send();
}
})
.child(Icon::new(IconType::Copy01).size(14.))
.text("Copy")
.into_element()
}
pub(crate) fn appshell_overlay(alpha: f32) -> Rect {
let alpha = alpha.clamp(0., 1.);
let scale = |a: u8| (f32::from(a) * alpha).round() as u8;