From c43272683bea1ac94df3bb32606949aa79cbe82c Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:17:52 +0200 Subject: [PATCH] feat: add code block and update the open link dialogs with the code block (#924) --- ..._are_selectable_and_links_can_be_copied.md | 5 + .../oneclient_app/src/components/button.rs | 26 +++++ .../src/components/code_block.rs | 107 ++++++++++++++++++ .../src/components/link_confirm.rs | 29 +++-- .../src/components/markdown/render.rs | 18 +-- packages/oneclient_app/src/components/mod.rs | 4 +- .../oneclient_app/src/layout/app_shell.rs | 67 ++--------- 7 files changed, 169 insertions(+), 87 deletions(-) create mode 100644 .changeset/code_blocks_are_selectable_and_links_can_be_copied.md create mode 100644 packages/oneclient_app/src/components/code_block.rs diff --git a/.changeset/code_blocks_are_selectable_and_links_can_be_copied.md b/.changeset/code_blocks_are_selectable_and_links_can_be_copied.md new file mode 100644 index 00000000..579bd199 --- /dev/null +++ b/.changeset/code_blocks_are_selectable_and_links_can_be_copied.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +# Code blocks are selectable, and the open link confirmation has a copy button diff --git a/packages/oneclient_app/src/components/button.rs b/packages/oneclient_app/src/components/button.rs index daf48271..bad5dd82 100644 --- a/packages/oneclient_app/src/components/button.rs +++ b/packages/oneclient_app/src/components/button.rs @@ -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") +} diff --git a/packages/oneclient_app/src/components/code_block.rs b/packages/oneclient_app/src/components/code_block.rs new file mode 100644 index 00000000..7c369b23 --- /dev/null +++ b/packages/oneclient_app/src/components/code_block.rs @@ -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, + height: Option, + key: DiffKey, +} + +impl KeyExt for CodeBlock { + fn write_key(&mut self) -> &mut DiffKey { + &mut self.key + } +} + +impl CodeBlock { + pub fn new(code: impl Into) -> 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()) + } +} diff --git a/packages/oneclient_app/src/components/link_confirm.rs b/packages/oneclient_app/src/components/link_confirm.rs index 9b882e61..767d3985 100644 --- a/packages/oneclient_app/src/components/link_confirm.rs +++ b/packages/oneclient_app/src/components/link_confirm.rs @@ -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() diff --git a/packages/oneclient_app/src/components/markdown/render.rs b/packages/oneclient_app/src/components/markdown/render.rs index 0461f7d1..1ba22ca6 100644 --- a/packages/oneclient_app/src/components/markdown/render.rs +++ b/packages/oneclient_app/src/components/markdown/render.rs @@ -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() diff --git a/packages/oneclient_app/src/components/mod.rs b/packages/oneclient_app/src/components/mod.rs index 64b1c8c5..fc519e2d 100644 --- a/packages/oneclient_app/src/components/mod.rs +++ b/packages/oneclient_app/src/components/mod.rs @@ -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}; diff --git a/packages/oneclient_app/src/layout/app_shell.rs b/packages/oneclient_app/src/layout/app_shell.rs index 9b206f5a..6164dfb0 100644 --- a/packages/oneclient_app/src/layout/app_shell.rs +++ b/packages/oneclient_app/src/layout/app_shell.rs @@ -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 = 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;