UI: status messages elide in the middle, the command line's placeholder fits its width

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 21:45:06 -04:00
1 parent 8b12cdd5c7
commit 2443746b35
3 files changed
+118 -5

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
//! Text that does not fit: elided in the middle, so both ends of a name stay readable
//! (`F2:side:a5#[F2…]|F2:side:a6`), with the full text left to a hover.
use std::borrow::Cow;
use egui::{FontId, Ui};
/// The ellipsis put in place of the middle.
pub const ELLIPSIS: char = '…';
/// `text`, or as much of its start and end as fits in `max_width` around an [`ELLIPSIS`].
/// `width_of` measures a string as drawn. Ties keep one more character at the start. With no room
/// even for the ellipsis alone, the ellipsis.
pub fn middle<'a>(text: &'a str, max_width: f32, width_of: impl Fn(&str) -> f32) -> Cow<'a, str> {
if width_of(text) <= max_width {
return Cow::Borrowed(text);
}
let chars: Vec<char> = text.chars().collect();
let joined = |kept: usize| -> String {
let head = kept.div_ceil(2);
let tail = kept - head;
let mut s: String = chars.iter().take(head).collect();
s.push(ELLIPSIS);
s.extend(chars.iter().skip(chars.len() - tail));
s
};
// The widest that fits: the width grows with the characters kept.
let (mut lo, mut hi) = (0, chars.len().saturating_sub(1));
while lo < hi {
let mid = (lo + hi).div_ceil(2);
if width_of(&joined(mid)) <= max_width {
lo = mid;
} else {
hi = mid - 1;
}
}
Cow::Owned(joined(lo))
}
/// The width of `text` on one line in `font`.
pub fn width(ui: &Ui, text: &str, font: &FontId) -> f32 {
ui.painter().layout_no_wrap(text.to_string(), font.clone(), egui::Color32::PLACEHOLDER).size().x
}
/// [`middle`] measured with `font` as `ui` draws it.
pub fn middle_in<'a>(ui: &Ui, text: &'a str, font: &FontId, max_width: f32) -> Cow<'a, str> {
middle(text, max_width, |s| width(ui, s, font))
}
#[cfg(test)]
mod tests {
use super::*;
/// One unit per character.
fn chars(s: &str) -> f32 {
s.chars().count() as f32
}
#[test]
fn fitting_text_is_kept_whole() {
assert!(matches!(middle("Seeds", 5.0, chars), Cow::Borrowed("Seeds")));
}
#[test]
fn long_text_keeps_both_ends() {
let name = "F2:side:a5#[F2:side:a6]|F2:side:a6";
let short = middle(name, 15.0, chars);
assert_eq!(short, "F2:side…side:a6");
assert!(chars(&short) <= 15.0);
assert_eq!(middle(name, 16.0, chars), "F2:side:…side:a6");
}
#[test]
fn no_room_leaves_the_ellipsis() {
assert_eq!(middle("abcdef", 1.0, chars), "…");
assert_eq!(middle("abcdef", 0.0, chars), "…");
assert_eq!(middle("", -1.0, chars), "…");
}
#[test]
fn multibyte_text_splits_on_characters() {
assert_eq!(middle("αβγδεζ", 4.0, chars), "αβ…ζ");
}
}
+1
View File
@@ -28,6 +28,7 @@ pub mod browser;
pub mod capture;
pub mod context;
pub mod control;
pub mod elide;
pub mod fields;
pub mod files;
pub mod form;
+33 -5
View File
@@ -102,6 +102,28 @@ pub fn submit(app: &mut ModelApp, line: &str) {
}
}
/// The command line's width: 40 % of the bar, within these.
const MIN_LINE_W: f32 = 150.0;
const MAX_LINE_W: f32 = 440.0;
/// The command line's placeholders, longest first: the longest that fits whole is shown.
const PLACEHOLDERS: [&str; 5] = [
r#"Type a command, e.g. mesh.node.add {"label": 1, "xyz": [0, 0, 0]} — or press S to search"#,
"Type a command, e.g. mesh.tet.generate — or press S to search",
"Type a command — or press S to search",
"Command, or S to search",
"Command",
];
/// The longest [`PLACEHOLDERS`] that fits a command line `width` wide, so it is never cut.
fn placeholder(ui: &egui::Ui, width: f32) -> &'static str {
let font = egui::TextStyle::Body.resolve(ui.style());
// A text field's default margin is 4 points a side.
let room = width - 8.0;
let shortest = PLACEHOLDERS[PLACEHOLDERS.len() - 1];
PLACEHOLDERS.iter().copied().find(|p| crate::elide::width(ui, p, &font) <= room).unwrap_or(shortest)
}
/// The command line, the status and the model's size, above the timeline.
pub fn command_line(app: &mut ModelApp, ui: &mut egui::Ui) {
let t = Tokens::get(app.ui.dark);
@@ -112,11 +134,11 @@ pub fn command_line(app: &mut ModelApp, ui: &mut egui::Ui) {
app.place("command_line", ui.max_rect());
ui.horizontal_centered(|ui| {
ui.label(RichText::new("›").strong().color(t.accent));
// The rest of the bar keeps room for the status and the model's size.
let width = (ui.available_width() * 0.4).clamp(MIN_LINE_W, MAX_LINE_W);
let hint = if app.command_line.is_empty() { placeholder(ui, width) } else { "" };
let r = ui.add(
egui::TextEdit::singleline(&mut app.command_line)
.id(egui::Id::new("fc_command_line_text"))
.hint_text(r#"Type a command, e.g. mesh.node.add {"label": 1, "xyz": [0, 0, 0]} — or press S to search"#)
.desired_width(440.0),
egui::TextEdit::singleline(&mut app.command_line).id(egui::Id::new("fc_command_line_text")).hint_text(hint).desired_width(width),
);
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
let line = std::mem::take(&mut app.command_line);
@@ -134,7 +156,13 @@ pub fn command_line(app: &mut ModelApp, ui: &mut egui::Ui) {
Some(s) => (s.text.as_str(), if s.error { t.error } else { t.text }),
None => ("Ready", t.text_dim),
};
ui.add(egui::Label::new(RichText::new(text).size(11.5).color(color)).truncate());
// Elided in the middle (both the subject and the outcome stay), whole on hover.
let font = FontId::proportional(11.5);
let shown = crate::elide::middle_in(ui, text, &font, ui.available_width());
let r = ui.add(egui::Label::new(RichText::new(shown.as_ref()).font(font).color(color)));
if shown != text {
r.on_hover_text(text);
}
});
});
});