feat: add code block and update the open link dialogs with the code block (#924)
This commit is contained in:
parent
cfcf7a7669
commit
c43272683b
7 changed files with 169 additions and 87 deletions
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
default: minor
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code blocks are selectable, and the open link confirmation has a copy button
|
||||||
|
|
@ -478,3 +478,29 @@ pub fn open_folder_button(folder: std::path::PathBuf) -> Button {
|
||||||
})
|
})
|
||||||
.child(Icon::new(IconType::Folder).size(16.))
|
.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")
|
||||||
|
}
|
||||||
|
|
|
||||||
107
packages/oneclient_app/src/components/code_block.rs
Normal file
107
packages/oneclient_app/src/components/code_block.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType, OVERLAY_BASE_LEVEL, OverlayPopup};
|
use crate::components::{
|
||||||
use crate::hooks::use_link_confirm;
|
Button, CodeBlock, Icon, IconType, OVERLAY_BASE_LEVEL, OverlayPopup, copy_button,
|
||||||
|
};
|
||||||
|
use crate::hooks::{use_dispatch, use_link_confirm};
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::ui::border_all_color;
|
use crate::ui::border_all_color;
|
||||||
|
|
||||||
|
|
@ -13,6 +15,7 @@ pub struct ConfirmLinkOverlay;
|
||||||
impl Component for ConfirmLinkOverlay {
|
impl Component for ConfirmLinkOverlay {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let mut pending = use_link_confirm();
|
let mut pending = use_link_confirm();
|
||||||
|
let dispatch = use_dispatch();
|
||||||
let Some(url) = pending.read().clone() else {
|
let Some(url) = pending.read().clone() else {
|
||||||
return rect().into_element();
|
return rect().into_element();
|
||||||
};
|
};
|
||||||
|
|
@ -73,20 +76,9 @@ impl Component for ConfirmLinkOverlay {
|
||||||
.color(colors::fg_secondary()),
|
.color(colors::fg_secondary()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
CodeBlock::new(url.clone())
|
||||||
.width(Size::fill())
|
.color(colors::code_info())
|
||||||
.padding(Gaps::new_all(10.))
|
.max_lines(4),
|
||||||
.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()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -94,6 +86,11 @@ impl Component for ConfirmLinkOverlay {
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.main_align(Alignment::End)
|
.main_align(Alignment::End)
|
||||||
.spacing(8.)
|
.spacing(8.)
|
||||||
|
.child(copy_button(
|
||||||
|
url.clone(),
|
||||||
|
"Link copied to your clipboard.",
|
||||||
|
dispatch,
|
||||||
|
))
|
||||||
.child(
|
.child(
|
||||||
Button::new()
|
Button::new()
|
||||||
.secondary()
|
.secondary()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use freya::prelude::*;
|
||||||
use super::MarkdownStyle;
|
use super::MarkdownStyle;
|
||||||
use super::image::MarkdownImage;
|
use super::image::MarkdownImage;
|
||||||
use super::parse::{Block, Inline, List, TextSpan};
|
use super::parse::{Block, Inline, List, TextSpan};
|
||||||
|
use crate::components::CodeBlock;
|
||||||
use crate::hooks::LinkConfirmState;
|
use crate::hooks::LinkConfirmState;
|
||||||
|
|
||||||
pub fn render_block(block: &Block, key: usize, style: &MarkdownStyle) -> Element {
|
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)
|
Block::Paragraph { content } => render_content(content, style.paragraph_size, style)
|
||||||
.key(key)
|
.key(key)
|
||||||
.into(),
|
.into(),
|
||||||
Block::Code { code } => rect()
|
Block::Code { code } => CodeBlock::new(code.clone())
|
||||||
.key(key)
|
.color(style.color_code)
|
||||||
.width(Size::fill())
|
|
||||||
.background(style.background_code)
|
.background(style.background_code)
|
||||||
.corner_radius(CornerRadius::new_all(6.))
|
.font_family(style.code_font_family.clone())
|
||||||
.padding(Gaps::new_all(12.))
|
.font_size(style.code_font_size)
|
||||||
.child(
|
.key(key)
|
||||||
label()
|
|
||||||
.text(code.clone())
|
|
||||||
.font_family(style.code_font_family.clone())
|
|
||||||
.font_size(style.code_font_size)
|
|
||||||
.color(style.color_code),
|
|
||||||
)
|
|
||||||
.into(),
|
.into(),
|
||||||
Block::List(list) => render_list(list, style).key(key).into(),
|
Block::List(list) => render_list(list, style).key(key).into(),
|
||||||
Block::Image { url, alt } => rect()
|
Block::Image { url, alt } => rect()
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ mod button;
|
||||||
mod charts;
|
mod charts;
|
||||||
mod checkbox;
|
mod checkbox;
|
||||||
mod cluster_update_popup;
|
mod cluster_update_popup;
|
||||||
|
mod code_block;
|
||||||
mod context_menu;
|
mod context_menu;
|
||||||
mod control_center;
|
mod control_center;
|
||||||
mod date_range_picker;
|
mod date_range_picker;
|
||||||
|
|
@ -62,10 +63,11 @@ pub use active_cluster_panel::ActiveClusterPanel;
|
||||||
pub use asset_image::AssetImage;
|
pub use asset_image::AssetImage;
|
||||||
pub use avatar::Avatar;
|
pub use avatar::Avatar;
|
||||||
pub use blocked_downloads_popup::BlockedDownloadsPopup;
|
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 charts::{BarChart, PieChart, ValueUnit, slice_color};
|
||||||
pub use checkbox::checkbox_labeled;
|
pub use checkbox::checkbox_labeled;
|
||||||
pub use cluster_update_popup::ClusterUpdatePopup;
|
pub use cluster_update_popup::ClusterUpdatePopup;
|
||||||
|
pub use code_block::CodeBlock;
|
||||||
pub use context_menu::ContextMenu;
|
pub use context_menu::ContextMenu;
|
||||||
pub use control_center::ControlCenter;
|
pub use control_center::ControlCenter;
|
||||||
pub use date_range_picker::{DateRange, DateRangePicker};
|
pub use date_range_picker::{DateRange, DateRangePicker};
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,10 @@ use skia_safe::sampling_options::CubicResampler;
|
||||||
use crate::Route;
|
use crate::Route;
|
||||||
use crate::components::Button;
|
use crate::components::Button;
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
ART_PREVIEW_EDGE, AppNavbar, DynamicArt, FileDropOverlay, Icon, IconType, OverlayPopup,
|
ART_PREVIEW_EDGE, AppNavbar, CodeBlock, DynamicArt, FileDropOverlay, Icon, IconType,
|
||||||
ScrollArea, accept_drop,
|
OverlayPopup, accept_drop, copy_button,
|
||||||
};
|
};
|
||||||
use crate::layout::AnimatedAppOutlet;
|
use crate::layout::AnimatedAppOutlet;
|
||||||
use crate::theme;
|
|
||||||
use crate::use_settings_snapshot;
|
use crate::use_settings_snapshot;
|
||||||
use oneclient_core::clusters::Cluster;
|
use oneclient_core::clusters::Cluster;
|
||||||
use oneclient_core::images::BACKGROUND_IMAGE_EDGE;
|
use oneclient_core::images::BACKGROUND_IMAGE_EDGE;
|
||||||
|
|
@ -121,20 +120,6 @@ struct LaunchErrorDialog {
|
||||||
impl Component for LaunchErrorDialog {
|
impl Component for LaunchErrorDialog {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let dispatch = crate::hooks::use_dispatch();
|
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 close = dispatch.clone();
|
||||||
let outside_close = dispatch.clone();
|
let outside_close = dispatch.clone();
|
||||||
let copy_dispatch = dispatch.clone();
|
let copy_dispatch = dispatch.clone();
|
||||||
|
|
@ -181,19 +166,9 @@ impl Component for LaunchErrorDialog {
|
||||||
.color(colors::fg_secondary()),
|
.color(colors::fg_secondary()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
CodeBlock::new(self.message.clone())
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::px(260.))
|
|
||||||
.background(CODE_BG)
|
.background(CODE_BG)
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
.height(260.),
|
||||||
.padding(Gaps::new_all(12.))
|
|
||||||
.overflow(Overflow::Clip)
|
|
||||||
.child(
|
|
||||||
ScrollArea::new()
|
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::fill())
|
|
||||||
.children(lines),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -201,7 +176,11 @@ impl Component for LaunchErrorDialog {
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.main_align(Alignment::End)
|
.main_align(Alignment::End)
|
||||||
.spacing(10.)
|
.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(
|
.child(
|
||||||
Button::new()
|
Button::new()
|
||||||
.primary()
|
.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 {
|
pub(crate) fn appshell_overlay(alpha: f32) -> Rect {
|
||||||
let alpha = alpha.clamp(0., 1.);
|
let alpha = alpha.clamp(0., 1.);
|
||||||
let scale = |a: u8| (f32::from(a) * alpha).round() as u8;
|
let scale = |a: u8| (f32::from(a) * alpha).round() as u8;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue