@@ -178,36 +178,49 @@ impl ConfirmDialog { |
| 178 | 178 | Rect::new(x, y, dialog_width, dialog_height) |
| 179 | 179 | } |
| 180 | 180 | |
| 181 | | - /// Wrap text to fit within max_width (simple word wrapping). |
| 182 | | - fn wrap_text(text: &str, max_chars: usize) -> Vec<String> { |
| 181 | + /// Wrap text to fit within max_width pixels using actual text measurement. |
| 182 | + fn wrap_text_to_width(text: &str, max_width: u32, renderer: &Renderer, style: &TextStyle) -> Vec<String> { |
| 183 | 183 | let mut lines = Vec::new(); |
| 184 | + |
| 184 | 185 | for line in text.lines() { |
| 185 | | - if line.len() <= max_chars { |
| 186 | | - lines.push(line.to_string()); |
| 187 | | - } else { |
| 188 | | - // Word wrap |
| 189 | | - let mut current_line = String::new(); |
| 190 | | - for word in line.split_whitespace() { |
| 191 | | - if current_line.is_empty() { |
| 192 | | - if word.len() > max_chars { |
| 193 | | - // Word too long, truncate with ellipsis |
| 194 | | - lines.push(format!("{}...", &word[..max_chars.saturating_sub(3)])); |
| 195 | | - } else { |
| 196 | | - current_line = word.to_string(); |
| 197 | | - } |
| 198 | | - } else if current_line.len() + 1 + word.len() <= max_chars { |
| 199 | | - current_line.push(' '); |
| 200 | | - current_line.push_str(word); |
| 201 | | - } else { |
| 202 | | - lines.push(current_line); |
| 203 | | - current_line = word.to_string(); |
| 204 | | - } |
| 186 | + // Check if the whole line fits |
| 187 | + if let Ok(metrics) = renderer.measure_text(line, style) { |
| 188 | + if metrics.width <= max_width { |
| 189 | + lines.push(line.to_string()); |
| 190 | + continue; |
| 205 | 191 | } |
| 206 | | - if !current_line.is_empty() { |
| 192 | + } |
| 193 | + |
| 194 | + // Need to wrap - build line word by word using pixel measurement |
| 195 | + let mut current_line = String::new(); |
| 196 | + for word in line.split_whitespace() { |
| 197 | + let test_line = if current_line.is_empty() { |
| 198 | + word.to_string() |
| 199 | + } else { |
| 200 | + format!("{} {}", current_line, word) |
| 201 | + }; |
| 202 | + |
| 203 | + let fits = renderer.measure_text(&test_line, style) |
| 204 | + .map(|m| m.width <= max_width) |
| 205 | + .unwrap_or(false); |
| 206 | + |
| 207 | + if fits { |
| 208 | + current_line = test_line; |
| 209 | + } else if current_line.is_empty() { |
| 210 | + // Single word doesn't fit - add it anyway (will overflow but better than infinite loop) |
| 211 | + lines.push(word.to_string()); |
| 212 | + } else { |
| 213 | + // Push current line and start new one with this word |
| 207 | 214 | lines.push(current_line); |
| 215 | + current_line = word.to_string(); |
| 208 | 216 | } |
| 209 | 217 | } |
| 218 | + |
| 219 | + if !current_line.is_empty() { |
| 220 | + lines.push(current_line); |
| 221 | + } |
| 210 | 222 | } |
| 223 | + |
| 211 | 224 | lines |
| 212 | 225 | } |
| 213 | 226 | |
@@ -264,13 +277,13 @@ impl ConfirmDialog { |
| 264 | 277 | .font_size(theme.font_size) |
| 265 | 278 | .color(theme.item_foreground); |
| 266 | 279 | |
| 267 | | - // Wrap and render message lines (use conservative char width estimate) |
| 268 | | - let max_chars = ((dialog_rect.width - 50) as f64 / (theme.font_size * 0.55)) as usize; |
| 269 | | - let wrapped_lines = Self::wrap_text(&self.message, max_chars.max(25)); |
| 280 | + // Wrap text using pixel-based measurement (40px padding on each side) |
| 281 | + let available_width = dialog_rect.width.saturating_sub(40); |
| 282 | + let wrapped_lines = Self::wrap_text_to_width(&self.message, available_width, renderer, &msg_style); |
| 270 | 283 | let mut y = dialog_rect.y + 56; |
| 271 | | - for line in wrapped_lines { |
| 284 | + for line in &wrapped_lines { |
| 272 | 285 | renderer.text( |
| 273 | | - &line, |
| 286 | + line, |
| 274 | 287 | (dialog_rect.x + 20) as f64, |
| 275 | 288 | y as f64, |
| 276 | 289 | &msg_style, |
@@ -299,10 +312,10 @@ impl ConfirmDialog { |
| 299 | 312 | .font_size(theme.font_size) |
| 300 | 313 | .color(theme.foreground); |
| 301 | 314 | |
| 302 | | - let confirm_text_width = renderer.measure_text(&self.confirm_label, &button_style)?.width; |
| 303 | | - let confirm_text_x = confirm_rect.x + (confirm_rect.width as i32 - confirm_text_width as i32) / 2; |
| 304 | | - let button_text_y = confirm_rect.y + (confirm_rect.height as i32 - theme.font_size as i32) / 2; |
| 305 | | - renderer.text(&self.confirm_label, confirm_text_x as f64, button_text_y as f64, &button_style)?; |
| 315 | + let confirm_metrics = renderer.measure_text(&self.confirm_label, &button_style)?; |
| 316 | + let confirm_text_x = confirm_rect.x + (confirm_rect.width as i32 - confirm_metrics.width as i32) / 2; |
| 317 | + let confirm_text_y = confirm_rect.y + (confirm_rect.height as i32 - confirm_metrics.height as i32) / 2; |
| 318 | + renderer.text(&self.confirm_label, confirm_text_x as f64, confirm_text_y as f64, &button_style)?; |
| 306 | 319 | |
| 307 | 320 | // Cancel button |
| 308 | 321 | let cancel_focused = self.focused_button == 1; |
@@ -317,9 +330,10 @@ impl ConfirmDialog { |
| 317 | 330 | renderer.stroke_rounded_rect(cancel_rect, 4.0, theme.foreground, 2.0)?; |
| 318 | 331 | } |
| 319 | 332 | |
| 320 | | - let cancel_text_width = renderer.measure_text(&self.cancel_label, &button_style)?.width; |
| 321 | | - let cancel_text_x = cancel_rect.x + (cancel_rect.width as i32 - cancel_text_width as i32) / 2; |
| 322 | | - renderer.text(&self.cancel_label, cancel_text_x as f64, button_text_y as f64, &button_style)?; |
| 333 | + let cancel_metrics = renderer.measure_text(&self.cancel_label, &button_style)?; |
| 334 | + let cancel_text_x = cancel_rect.x + (cancel_rect.width as i32 - cancel_metrics.width as i32) / 2; |
| 335 | + let cancel_text_y = cancel_rect.y + (cancel_rect.height as i32 - cancel_metrics.height as i32) / 2; |
| 336 | + renderer.text(&self.cancel_label, cancel_text_x as f64, cancel_text_y as f64, &button_style)?; |
| 323 | 337 | |
| 324 | 338 | Ok(()) |
| 325 | 339 | } |
@@ -906,9 +920,9 @@ impl ConflictDialog { |
| 906 | 920 | renderer.stroke_rounded_rect(*rect, 4.0, theme.foreground, 2.0)?; |
| 907 | 921 | } |
| 908 | 922 | |
| 909 | | - let text_width = renderer.measure_text(label, &button_style)?.width; |
| 910 | | - let text_x = rect.x + (rect.width as i32 - text_width as i32) / 2; |
| 911 | | - let text_y = rect.y + (rect.height as i32 - (theme.font_size - 1.0) as i32) / 2; |
| 923 | + let text_metrics = renderer.measure_text(label, &button_style)?; |
| 924 | + let text_x = rect.x + (rect.width as i32 - text_metrics.width as i32) / 2; |
| 925 | + let text_y = rect.y + (rect.height as i32 - text_metrics.height as i32) / 2; |
| 912 | 926 | renderer.text(label, text_x as f64, text_y as f64, &button_style)?; |
| 913 | 927 | } |
| 914 | 928 | |
@@ -1245,10 +1259,10 @@ impl InputDialog { |
| 1245 | 1259 | } |
| 1246 | 1260 | |
| 1247 | 1261 | let ok_text = "OK"; |
| 1248 | | - let ok_width = renderer.measure_text(ok_text, &button_style)?.width; |
| 1249 | | - let ok_x = ok_rect.x + (ok_rect.width as i32 - ok_width as i32) / 2; |
| 1250 | | - let button_text_y = ok_rect.y + (ok_rect.height as i32 - theme.font_size as i32) / 2; |
| 1251 | | - renderer.text(ok_text, ok_x as f64, button_text_y as f64, &button_style)?; |
| 1262 | + let ok_metrics = renderer.measure_text(ok_text, &button_style)?; |
| 1263 | + let ok_x = ok_rect.x + (ok_rect.width as i32 - ok_metrics.width as i32) / 2; |
| 1264 | + let ok_text_y = ok_rect.y + (ok_rect.height as i32 - ok_metrics.height as i32) / 2; |
| 1265 | + renderer.text(ok_text, ok_x as f64, ok_text_y as f64, &button_style)?; |
| 1252 | 1266 | |
| 1253 | 1267 | // Cancel button |
| 1254 | 1268 | let cancel_focused = self.focused == 2; |
@@ -1264,9 +1278,10 @@ impl InputDialog { |
| 1264 | 1278 | } |
| 1265 | 1279 | |
| 1266 | 1280 | let cancel_text = "Cancel"; |
| 1267 | | - let cancel_width = renderer.measure_text(cancel_text, &button_style)?.width; |
| 1268 | | - let cancel_x = cancel_rect.x + (cancel_rect.width as i32 - cancel_width as i32) / 2; |
| 1269 | | - renderer.text(cancel_text, cancel_x as f64, button_text_y as f64, &button_style)?; |
| 1281 | + let cancel_metrics = renderer.measure_text(cancel_text, &button_style)?; |
| 1282 | + let cancel_x = cancel_rect.x + (cancel_rect.width as i32 - cancel_metrics.width as i32) / 2; |
| 1283 | + let cancel_text_y = cancel_rect.y + (cancel_rect.height as i32 - cancel_metrics.height as i32) / 2; |
| 1284 | + renderer.text(cancel_text, cancel_x as f64, cancel_text_y as f64, &button_style)?; |
| 1270 | 1285 | |
| 1271 | 1286 | Ok(()) |
| 1272 | 1287 | } |