From 7421b225b789704be71a21e89e86336452154e55 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Fri, 17 Apr 2026 21:25:22 -0700 Subject: [PATCH] Use Ellipsis wrap instead of string truncation. Fix image viewer --- src/home/link_preview.rs | 50 ++++++------- src/shared/image_viewer.rs | 140 +++++++++++++------------------------ src/shared/timestamp.rs | 2 - 3 files changed, 67 insertions(+), 125 deletions(-) diff --git a/src/home/link_preview.rs b/src/home/link_preview.rs index 7e2528d9c..aa4ce9c00 100644 --- a/src/home/link_preview.rs +++ b/src/home/link_preview.rs @@ -7,7 +7,7 @@ use std::{ }; use makepad_widgets::*; -use crate::{LivePtr, widget_ref_from_live_ptr}; +use crate::{LivePtr, utils, widget_ref_from_live_ptr}; use matrix_sdk::ruma::{events::room::{ImageInfo, MediaSource}, OwnedMxcUri, UInt}; use serde::Deserialize; use url::Url; @@ -19,8 +19,6 @@ use crate::{ sliding_sync::{submit_async_request, MatrixRequest, UrlPreviewError}, }; -/// Maximum length for link preview descriptions before truncation -const MAX_DESCRIPTION_LENGTH: usize = 180; /// Maximum number of cache entries before cleanup is triggered const MAX_CACHE_ENTRIES_BEFORE_CLEANUP: usize = 100; /// Maximum age for cache entries in seconds (1 hour) @@ -165,7 +163,7 @@ script_mod! { width: Fill, height: Fill, flow: Down, - View { + inner_content_view := View { width: Fit, height: Fit, flow: Flow.Right{wrap: true}, @@ -193,21 +191,18 @@ script_mod! { } } - View { - width: Fill, height: Fit, - description_label := Label { - width: Fill, height: Fit, - flow: Flow.Right{wrap: true}, - padding: Inset{ left: 0.0 } - max_lines: 2 - text_overflow: Ellipsis - draw_text +: { - text_style: mod.widgets.LINK_PREVIEW_MESSAGE_TEXT_STYLE { - font_size: 11.0, - }, - color: #666666, - } + description_label := Label { + width: Fill, height: Fit, + flow: Flow.Right{wrap: true}, + padding: Inset{ left: 0.0 } + max_lines: 2 + text_overflow: Ellipsis + draw_text +: { + text_style: mod.widgets.LINK_PREVIEW_MESSAGE_TEXT_STYLE { + font_size: 11.0, + }, + color: #666666, } } } @@ -408,24 +403,19 @@ impl LinkPreviewRef { // Set site name if let Some(site_name) = &link_preview_data.site_name { view_ref - .view(cx, ids!(content_view)) .label(cx, ids!(site_name_label)) .set_text(cx, site_name); } - // Set description with size limit - if let Some(description) = &link_preview_data.description { - let mut description = description.clone(); - description = description.replace("\n\n", " "); - let truncated_description = if description.len() > MAX_DESCRIPTION_LENGTH { - format!("{}...", &description[..(MAX_DESCRIPTION_LENGTH - 3)]) - } else { - description - }; + // Set description; the description_label uses max_lines: 2 with + // text_overflow: Ellipsis to wrap and truncate automatically. + // Collapse all whitespace runs into single spaces so hard line breaks + // don't burn one of the two available lines. + if let Some(description_raw) = &link_preview_data.description { + let description = utils::replace_linebreaks_separators(description_raw, false); view_ref - .view(cx, ids!(content_view)) .label(cx, ids!(description_label)) - .set_text(cx, &truncated_description); + .set_text(cx, &description); } // Handle image through closure diff --git a/src/shared/image_viewer.rs b/src/shared/image_viewer.rs index 138ce064c..2b6d5aa96 100644 --- a/src/shared/image_viewer.rs +++ b/src/shared/image_viewer.rs @@ -205,7 +205,7 @@ script_mod! { width: Fill, height: Fit flow: Right align: Align{y: 0.5, x: 0.0} - padding: 13 + padding: Inset{top: 13, bottom: 8, left: 13, right: 13} spacing: 8, show_bg: true @@ -214,14 +214,13 @@ script_mod! { color: (COLOR_IMAGE_VIEWER_META_BACKGROUND) } - // Display user profile view below the button group when the width is not enough. - user_profile_view := View { - width: Fit, - height: Fit, - flow: Right, - spacing: 13, - align: Align{ y: 0.5 } - + avatar_timestamp_view := View { + width: Fit + height: Fit + flow: Down + spacing: 2 + align: Align{x: 0.5, y: 0.0} + avatar := Avatar { width: 45, height: 45, text_view +: { @@ -232,55 +231,54 @@ script_mod! { } } } - - content := View { - width: Fit + timestamp := Timestamp { + width: Fit, height: Fit, - align: Align{ y: 0.5 } - spacing: 3 - flow: Down, - - username := Label { - width: Fit - height: Fit, - padding: 0 - margin: 0 - flow: Right + ts_label := Label { draw_text +: { - text_style: REGULAR_TEXT {font_size: 12}, + text_style: theme.font_regular {font_size: 9.5}, color: (COLOR_TEXT) } } + } + } - timestamp_view := View { - width: Fit - height: Fit - - timestamp := Timestamp { - width: Fit, - height: Fit, - ts_label := Label { - draw_text +: { - text_style: theme.font_regular {font_size: 9.5}, - color: (COLOR_TEXT) - } - } - } + username_label_view := View { + width: Fill{weight: 0.35}, + // width: Fill, + height: Fit, + flow: Right, + align: Align{ y: 0.5 } + + username := Label { + width: Fill, + height: Fit, + padding: 0 + margin: 0 + flow: Flow.Right{wrap: true} + max_lines: 2 + text_overflow: Ellipsis + draw_text +: { + text_style: REGULAR_TEXT {font_size: 12}, + color: (COLOR_TEXT) } } } - // Display image name and size below the user_profile_view if the width is not enough. + // Display image name and size below the username when the width is not enough. image_name_and_size_view := View { - width: Fill + width: Fill{weight: 0.65}, + // width: Fill height: Fit, - align: Align{x: 0.5, y: 0.5} + align: Align{x: 0, y: 0.5} flow: Right image_name_and_size := Label { width: Fill, height: Fit, - align: Align{x: 0.5, y: 0.5} + align: Align{x: 0, y: 0.5} flow: Flow.Right{wrap: true} + max_lines: 2 + text_overflow: Ellipsis draw_text +: { text_style: REGULAR_TEXT {font_size: 13}, color: (COLOR_TEXT), @@ -1099,29 +1097,23 @@ impl ImageViewer { /// Sets the metadata view in the image viewer with the provided metadata. /// - /// The metadata view is updated with the truncated image name and the human-readable size of the image. - /// - /// The image name is truncated to 24 characters and appended with "..." if it exceeds the limit. - /// The human-readable size is calculated based on the image size in bytes. + /// The image_name_and_size and username labels handle their own overflow + /// via `max_lines: 2` + `text_overflow: Ellipsis` in the layout. pub fn set_metadata(&mut self, cx: &mut Cx, metadata: &ImageViewerMetaData) { let meta_view = self.view.view(cx, ids!(metadata_view)); - let truncated_name = truncate_image_name(&metadata.image_name); let human_readable_size = format_file_size(metadata.image_file_size); - let display_text = format!("{} ({})", truncated_name, human_readable_size); + let display_text = format!("{} ({})", metadata.image_name, human_readable_size); meta_view .label(cx, ids!(image_name_and_size)) .set_text(cx, &display_text); if let Some(timestamp) = metadata.timestamp { meta_view - .view(cx, ids!(user_profile_view.content.timestamp_view)) - .set_visible(cx, true); - meta_view - .timestamp(cx, ids!(user_profile_view.content.timestamp_view.timestamp)) + .timestamp(cx, ids!(avatar_timestamp_view.timestamp)) .set_date_time(cx, timestamp); } if let Some((timeline_kind, event_timeline_item)) = &metadata.avatar_parameter { - let (sender, _) = self.view.avatar(cx, ids!(user_profile_view.avatar)).set_avatar_and_get_username( + let (sender, _) = self.view.avatar(cx, ids!(avatar_timestamp_view.avatar)).set_avatar_and_get_username( cx, timeline_kind, event_timeline_item.sender(), @@ -1129,15 +1121,9 @@ impl ImageViewer { event_timeline_item.event_id(), false, ); - if sender.len() > MAX_USERNAME_LENGTH { - meta_view - .label(cx, ids!(user_profile_view.content.username)) - .set_text(cx, &format!("{}...", &sender[..MAX_USERNAME_LENGTH - 3])); - } else { - meta_view - .label(cx, ids!(user_profile_view.content.username)) - .set_text(cx, &sender); - }; + meta_view + .label(cx, ids!(username_label_view.username)) + .set_text(cx, &sender); } } } @@ -1217,38 +1203,6 @@ pub struct ImageViewerMetaData { pub image_file_size: u64, } -/// Maximum image name length to be displayed -const MAX_IMAGE_NAME_LENGTH: usize = 50; -/// Maximum username length to be displayed -const MAX_USERNAME_LENGTH: usize = 50; - -/// Truncate image name while preserving file extension -fn truncate_image_name(image_name: &str) -> String { - let max_length = MAX_IMAGE_NAME_LENGTH; - - if image_name.len() <= max_length { - return image_name.to_string(); - } - - // Find the last dot to separate name and extension - if let Some(dot_pos) = image_name.rfind('.') { - let name_part = &image_name[..dot_pos]; - let extension = &image_name[dot_pos..]; - - // Reserve space for "..." and the extension - let available_length = max_length.saturating_sub(3 + extension.len()); - - if available_length > 0 && name_part.len() > available_length { - format!("{}...{}", &name_part[..available_length], extension) - } else { - image_name.to_string() - } - } else { - // No extension found, just truncate the name - format!("{}...", &image_name[..max_length.saturating_sub(3)]) - } -} - /// Convert bytes to human-readable file size format fn format_file_size(bytes: u64) -> String { const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; diff --git a/src/shared/timestamp.rs b/src/shared/timestamp.rs index c84935fd3..31c8b720d 100644 --- a/src/shared/timestamp.rs +++ b/src/shared/timestamp.rs @@ -11,8 +11,6 @@ script_mod! { mod.widgets.Timestamp = #(Timestamp::register_widget(vm)) { - - width: Fit, height: Fit flow: Right,