@@ -16,7 +16,7 @@ use std::path::PathBuf; |
| 16 | 16 | const MAX_VISIBLE_ITEMS: usize = 8; |
| 17 | 17 | |
| 18 | 18 | /// Item height in pixels (name + description + padding). |
| 19 | | -const ITEM_HEIGHT: u32 = 48; |
| 19 | +const ITEM_HEIGHT: u32 = 54; |
| 20 | 20 | |
| 21 | 21 | /// Input field height. |
| 22 | 22 | const INPUT_HEIGHT: u32 = 36; |
@@ -109,6 +109,46 @@ fn clean_exec_command(exec: &str) -> String { |
| 109 | 109 | result.trim().to_string() |
| 110 | 110 | } |
| 111 | 111 | |
| 112 | +/// Truncate text to fit within a given pixel width, adding ellipsis if needed. |
| 113 | +fn truncate_to_width(text: &str, max_width: f64, style: &TextStyle, renderer: &Renderer) -> String { |
| 114 | + let max_width = max_width as u32; |
| 115 | + |
| 116 | + // First check if it fits |
| 117 | + if let Ok(metrics) = renderer.measure_text(text, style) { |
| 118 | + if metrics.width <= max_width { |
| 119 | + return text.to_string(); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + // Binary search for the right length |
| 124 | + let ellipsis = "..."; |
| 125 | + let ellipsis_width = renderer.measure_text(ellipsis, style) |
| 126 | + .map(|m| m.width) |
| 127 | + .unwrap_or(20); |
| 128 | + |
| 129 | + if max_width <= ellipsis_width { |
| 130 | + return ellipsis.to_string(); |
| 131 | + } |
| 132 | + |
| 133 | + let target_width = max_width - ellipsis_width; |
| 134 | + |
| 135 | + // Start from full text and shrink |
| 136 | + let chars: Vec<char> = text.chars().collect(); |
| 137 | + let mut end = chars.len(); |
| 138 | + |
| 139 | + while end > 0 { |
| 140 | + let substr: String = chars[..end].iter().collect(); |
| 141 | + if let Ok(metrics) = renderer.measure_text(&substr, style) { |
| 142 | + if metrics.width <= target_width { |
| 143 | + return format!("{}{}", substr, ellipsis); |
| 144 | + } |
| 145 | + } |
| 146 | + end -= 1; |
| 147 | + } |
| 148 | + |
| 149 | + ellipsis.to_string() |
| 150 | +} |
| 151 | + |
| 112 | 152 | /// Get XDG application directories to scan. |
| 113 | 153 | fn get_app_directories() -> Vec<PathBuf> { |
| 114 | 154 | let mut dirs = Vec::new(); |
@@ -643,29 +683,28 @@ impl AppPickerDialog { |
| 643 | 683 | renderer.fill_rounded_rect(item_rect, 4.0, theme.item_hover_background)?; |
| 644 | 684 | } |
| 645 | 685 | |
| 646 | | - // App name (positioned near top of item) |
| 647 | | - let name_y = item_y + 10; |
| 686 | + // Available width for text (with padding on both sides) |
| 687 | + let text_x = item_rect.x + 12; |
| 688 | + let available_width = (item_rect.width as i32 - 24) as f64; |
| 689 | + |
| 690 | + // App name (positioned near top of item with more padding) |
| 691 | + let name_y = item_y + 12; |
| 648 | 692 | renderer.text( |
| 649 | 693 | &app.name, |
| 650 | | - (item_rect.x + 12) as f64, |
| 694 | + text_x as f64, |
| 651 | 695 | name_y as f64, |
| 652 | 696 | &name_style, |
| 653 | 697 | )?; |
| 654 | 698 | |
| 655 | 699 | // App description (if any, positioned below name with gap) |
| 656 | 700 | if let Some(desc) = &app.description { |
| 657 | | - // Truncate long descriptions |
| 658 | | - let max_desc_len = 70; |
| 659 | | - let truncated = if desc.len() > max_desc_len { |
| 660 | | - format!("{}...", &desc[..max_desc_len]) |
| 661 | | - } else { |
| 662 | | - desc.clone() |
| 663 | | - }; |
| 664 | | - |
| 665 | | - let desc_y = name_y + (theme.font_size as i32) + 6; |
| 701 | + // Truncate description to fit available width |
| 702 | + let truncated = truncate_to_width(desc, available_width, &desc_style, renderer); |
| 703 | + |
| 704 | + let desc_y = name_y + (theme.font_size as i32) + 8; |
| 666 | 705 | renderer.text( |
| 667 | 706 | &truncated, |
| 668 | | - (item_rect.x + 12) as f64, |
| 707 | + text_x as f64, |
| 669 | 708 | desc_y as f64, |
| 670 | 709 | &desc_style, |
| 671 | 710 | )?; |