Phase 6-07a: Fensterlokale Control-Menüs und Windows-Testkorrekturen

Control-Menüs öffnen für alle Fensterarten am [≡]-Symbol des aktiven
Fensters: eigener Auswahlzustand statt Kopplung an die Menüleiste,
bevorzugt unter der Titelzeile, bei Platzmangel nach oben aufgeklappt,
am rechten Rand nur so weit nach links wie nötig. Regressionstest über
TestBackend für Code-, Projekt-, Output-, Immediate-, Debug- und
Hilfefenster, maximiert, minimiert, Rand und Maus.

Nebenbefunde der Windows-Testausführung: Temporärdatei vor sync_all
schreibend öffnen (Zugriff verweigert), relative Projektverweise immer
mit / schreiben, zeilenendenneutrale Vergleiche in Referenzmatrix-,
Kompatibilitäts- und MAK-Beispieltests, DriveListBox-Zeichenbild
plattformneutral. Korpus-Sollausgaben per .gitattributes auf LF.

Specs ide-oberflaeche und ide-projekte synchronisiert, Change archiviert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 11:02:40 +02:00
parent 6e742d96b1
commit 5a1a284505
22 changed files with 572 additions and 107 deletions

View File

@@ -302,7 +302,7 @@ fn windows_restore_geometry_and_resize_without_document_loss() {
assert!(r.right() <= 80 && r.bottom() <= 24);
}
key(&mut app, K::Char('-'), M::ALT);
assert!(app.control_menu);
assert!(app.control_menu.is_some());
plain(&mut app, K::Esc);
key(&mut app, K::F(4), M::CONTROL);
assert_eq!(app.windows.len(), 2);
@@ -1180,3 +1180,182 @@ fn theme_windows_erase_desktop_and_underlying_windows_on_every_redraw() {
}
}
}
/// Sucht die Zelle, an der `text` in der Ausgabe beginnt.
fn find_text(b: &ratatui::buffer::Buffer, text: &str) -> Option<(u16, u16)> {
let chars: Vec<String> = text.chars().map(|c| c.to_string()).collect();
for y in 0..b.area.height {
for x in 0..b.area.width.saturating_sub(chars.len() as u16) {
if chars
.iter()
.enumerate()
.all(|(i, c)| b[(x + i as u16, y)].symbol() == c)
{
return Some((x, y));
}
}
}
None
}
/// Öffnet das Control-Menü per Alt+Minus und prüft die Anheftregel: linke Spalte gleich
/// Symbolspalte (oder so weit links, dass es passt und das Symbol überdeckt), Rahmen grenzt
/// unten oder oben an die Titelzeile, vollständig in der Arbeitsfläche, kein Menüleistentitel
/// hervorgehoben. Liefert das Rechteck des Aufklappbereichs.
fn assert_control_menu_attached(app: &mut App, context: &str) -> ratatui::layout::Rect {
let r = app.rect(app.active_window().unwrap());
let area = app.area();
key(app, K::Char('-'), M::ALT);
assert!(app.control_menu.is_some(), "{context}: Control-Menü offen");
assert_eq!(
app.menu, None,
"{context}: Menüleiste darf nicht ausgewählt sein"
);
let (_, b) = draw(app);
assert_eq!(
(b[(1, 0)].fg, b[(1, 0)].bg),
(tb_ide::render::dos(0), tb_ide::render::dos(7)),
"{context}: „File“ darf nicht hervorgehoben sein"
);
let (rx, ry) = find_text(&b, "Restore").unwrap_or_else(|| panic!("{context}: Restore"));
let (_, cy) = find_text(&b, "Close").unwrap_or_else(|| panic!("{context}: Close"));
let (x, top, bottom) = (rx - 1, ry - 1, cy + 1);
let mut right = x + 1;
while right < b.area.width && b[(right, top)].symbol() != "" {
right += 1;
}
let popup = ratatui::layout::Rect::new(x, top, right + 1 - x, bottom + 1 - top);
assert_eq!(b[(x, top)].symbol(), "", "{context}: linke obere Ecke");
assert_eq!(b[(x, bottom)].symbol(), "", "{context}: linke untere Ecke");
assert!(
popup.y >= area.y && popup.bottom() <= area.bottom() && popup.right() <= area.right(),
"{context}: Popup {popup:?} außerhalb der Arbeitsfläche {area:?}"
);
assert!(
popup.x <= r.x && r.x < popup.right(),
"{context}: Popup {popup:?} überdeckt die Symbolspalte von {r:?} nicht"
);
if r.x + popup.width <= area.right() {
assert_eq!(popup.x, r.x, "{context}: linke Spalte gleich Symbolspalte");
} else {
assert_eq!(
popup.right(),
area.right(),
"{context}: nur so weit links wie nötig"
);
}
if popup.height <= area.bottom() - (r.y + 1) {
assert_eq!(popup.y, r.y + 1, "{context}: direkt unter der Titelzeile");
} else {
assert_eq!(
popup.bottom(),
r.y,
"{context}: nach oben aufgeklappt bis an die Titelzeile"
);
}
popup
}
#[test]
fn control_menus_attach_to_the_symbol_of_every_window() {
let t = Temp::new();
let mut app = t.app();
let area = app.area();
let code = app.active;
let p = assert_control_menu_attached(&mut app, "Code");
let r = app.rect(app.active_window().unwrap());
assert_eq!((p.x, p.y), (r.x, r.y + 1));
plain(&mut app, K::Esc);
assert!(app.control_menu.is_none());
assert_eq!(app.active, code);
plain(&mut app, K::F(6));
assert_eq!(app.active_window().unwrap().kind, WindowKind::Project);
assert_control_menu_attached(&mut app, "Project");
plain(&mut app, K::Esc);
menu(&mut app, Command::Output);
assert_eq!(app.active_window().unwrap().kind, WindowKind::Output);
assert_control_menu_attached(&mut app, "Output");
plain(&mut app, K::Esc);
menu(&mut app, Command::Immediate);
assert_eq!(app.active_window().unwrap().kind, WindowKind::Immediate);
assert_control_menu_attached(&mut app, "Immediate");
plain(&mut app, K::Esc);
menu(&mut app, Command::Debug);
assert_eq!(app.active_window().unwrap().kind, WindowKind::Debug);
assert_control_menu_attached(&mut app, "Debug");
plain(&mut app, K::Esc);
menu(&mut app, Command::HelpWindow);
assert_eq!(app.active_window().unwrap().kind, WindowKind::Help);
assert_control_menu_attached(&mut app, "Help");
plain(&mut app, K::Esc);
assert_eq!(app.active_window().unwrap().kind, WindowKind::Help);
menu(&mut app, Command::Immediate);
key(&mut app, K::F(10), M::CONTROL);
let p = assert_control_menu_attached(&mut app, "Maximized");
assert_eq!((p.x, p.y), (area.x, area.y + 1));
plain(&mut app, K::Esc);
key(&mut app, K::F(9), M::CONTROL);
assert_eq!(app.active_window().unwrap().state, WindowState::Minimized);
let r = app.rect(app.active_window().unwrap());
let p = assert_control_menu_attached(&mut app, "Minimized");
assert_eq!(
(p.x, p.bottom()),
(r.x, r.y),
"minimiertes Fenster klappt nach oben"
);
plain(&mut app, K::Esc);
key(&mut app, K::F(5), M::CONTROL);
key(&mut app, K::F(8), M::CONTROL);
for _ in 0..20 {
key(&mut app, K::Left, M::CONTROL);
}
plain(&mut app, K::Enter);
key(&mut app, K::F(7), M::CONTROL);
for _ in 0..30 {
key(&mut app, K::Right, M::CONTROL);
}
plain(&mut app, K::Enter);
let r = app.rect(app.active_window().unwrap());
assert_eq!(r.right(), area.right());
let p = assert_control_menu_attached(&mut app, "Rechter Rand");
assert!(p.x < r.x && p.right() == area.right());
plain(&mut app, K::Esc);
// Normales Fenster am unteren Rand: zu wenig Platz unter der Titelzeile, klappt nach oben.
key(&mut app, K::F(8), M::CONTROL);
for _ in 0..20 {
key(&mut app, K::Up, M::CONTROL);
}
plain(&mut app, K::Enter);
key(&mut app, K::F(7), M::CONTROL);
for _ in 0..30 {
key(&mut app, K::Down, M::CONTROL);
}
plain(&mut app, K::Enter);
let r = app.rect(app.active_window().unwrap());
assert_eq!(r.bottom(), area.bottom());
let p = assert_control_menu_attached(&mut app, "Unterer Rand");
assert_eq!(p.bottom(), r.y, "klappt nach oben");
plain(&mut app, K::Esc);
// Maus: Klick auf [≡] öffnet identisch, Klick auf einen Eintrag führt ihn aus.
key(&mut app, K::F(5), M::CONTROL);
draw(&mut app);
let r = app.rect(app.active_window().unwrap());
click(&mut app, r.x, r.y);
assert!(app.control_menu.is_some());
assert_eq!(app.menu, None);
let (_, b) = draw(&mut app);
let (mx, my) = find_text(&b, "Minimize").unwrap();
click(&mut app, mx, my);
assert!(app.control_menu.is_none());
assert_eq!(app.active_window().unwrap().state, WindowState::Minimized);
}