feat: make package descriptions selectable and copyable (#1010)

* feat: make package descriptions selectable and copyable

- Replace text widget with text_editor for package descriptions
- Add read-only text_editor that allows text selection and copying
- Implement Clone manually for List struct to handle text_editor::Content
- Add text_editor StyleSheet implementation to match app theme

This improves UX by allowing users to copy package descriptions for
research or sharing purposes while maintaining the same visual appearance.

---------

Co-authored-by: Samuele Domenico Ruffino <sdruffino@it.iliad.com>
This commit is contained in:
samuele.ruffino 2025-07-10 10:04:32 +02:00 committed by GitHub
commit d471b798c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 67 additions and 5 deletions

View file

@ -42,7 +42,7 @@ pub struct UpdateState {
uad_list: UadListState,
}
#[derive(Default, Clone)]
#[derive(Default)]
pub struct UadGui {
view: View,
apps_view: AppsView,

View file

@ -1,5 +1,6 @@
use crate::core::theme::Theme;
use iced::overlay::menu;
use iced::widget::text_editor;
use iced::widget::{
button, checkbox, container, pick_list, radio, rule, scrollable, text, text_input,
};
@ -565,6 +566,50 @@ impl radio::StyleSheet for Theme {
}
}
impl text_editor::StyleSheet for Theme {
type Style = ();
fn active(&self, _style: &Self::Style) -> text_editor::Appearance {
let p = self.palette();
text_editor::Appearance {
background: Background::Color(p.base.foreground),
border: Border {
width: 0.0,
radius: 0.0.into(),
color: Color::TRANSPARENT,
},
}
}
fn focused(&self, style: &Self::Style) -> text_editor::Appearance {
self.active(style)
}
fn placeholder_color(&self, _style: &Self::Style) -> Color {
self.palette().normal.surface
}
fn value_color(&self, _style: &Self::Style) -> Color {
self.palette().bright.surface
}
fn disabled_color(&self, _style: &Self::Style) -> Color {
self.palette().normal.surface
}
fn selection_color(&self, _style: &Self::Style) -> Color {
let p = self.palette();
Color {
a: 0.3,
..p.normal.primary
}
}
fn disabled(&self, style: &Self::Style) -> text_editor::Appearance {
self.active(style)
}
}
#[derive(Default, Clone, Copy)]
pub enum Rule {
#[default]

View file

@ -16,7 +16,7 @@ use crate::gui::widgets::package_row::{Message as RowMessage, PackageRow};
use crate::gui::widgets::text;
use iced::widget::{
Column, Space, button, checkbox, column, container, horizontal_space, pick_list, radio, row,
scrollable, text_input, tooltip, vertical_rule,
scrollable, text_editor, text_input, tooltip, vertical_rule,
};
use iced::{Alignment, Command, Element, Length, Renderer, alignment};
@ -39,7 +39,7 @@ pub enum LoadingState {
FailedToUpdate,
}
#[derive(Default, Debug, Clone)]
#[derive(Default, Debug)]
#[allow(clippy::struct_excessive_bools, reason = "Not a state-machine")]
pub struct List {
pub loading_state: LoadingState,
@ -57,6 +57,7 @@ pub struct List {
all_selected: bool,
pub input_value: String,
description: String,
description_content: text_editor::Content,
selection_modal: bool,
error_modal: Option<String>,
export_modal: bool,
@ -89,6 +90,7 @@ pub enum Message {
GoToUrl(PathBuf),
ExportSelection,
SelectionExported(Result<bool, String>),
DescriptionEdit(text_editor::Action),
}
pub struct SummaryEntry {
@ -280,6 +282,8 @@ impl List {
}
RowMessage::PackagePressed => {
self.description = package.clone().description;
self.description_content =
text_editor::Content::with_text(&package.description);
package.current = true;
if self.current_package_index != i_package {
self.phone_packages[i_user][self.current_package_index].current = false;
@ -352,6 +356,18 @@ impl List {
Command::none()
}
Message::Nothing => Command::none(),
Message::DescriptionEdit(action) => {
match action {
text_editor::Action::Edit(_) => {
// Do nothing - ignore all editing operations
}
// Allow all other actions (movement, selection, clicking, scrolling, etc.)
_ => {
self.description_content.perform(action);
}
}
Command::none()
}
}
}
@ -487,8 +503,9 @@ impl List {
.height(Length::FillPortion(6))
.style(style::Scrollable::Packages);
let description_scroll = scrollable(text(&self.description).width(Length::Fill))
.style(style::Scrollable::Description);
let description_scroll =
scrollable(text_editor(&self.description_content).on_action(Message::DescriptionEdit))
.style(style::Scrollable::Description);
let description_panel = container(description_scroll)
.padding(6)