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:
@@ -801,7 +801,10 @@ fn reference_matrix_is_complete_and_points_to_runnable_tests() {
|
||||
assert!(!tests.is_empty());
|
||||
for test in tests {
|
||||
let (file, function) = test.split_once("::").unwrap();
|
||||
let source = fs::read_to_string(root.join(file)).unwrap();
|
||||
// Quellen liegen je Checkout mit CRLF vor; die Suche ist zeilenendenneutral.
|
||||
let source = fs::read_to_string(root.join(file))
|
||||
.unwrap()
|
||||
.replace("\r\n", "\n");
|
||||
assert!(
|
||||
source.contains(&format!("#[test]\nfn {function}(")),
|
||||
"{test}"
|
||||
|
||||
@@ -478,6 +478,9 @@ fn source_for_directives(path: &Path) -> String {
|
||||
/// Snapshots vergleichen: erst das Textbild, dann die Attributebene.
|
||||
/// Bei Abweichung im Text zählt die Zeile, bei Attributen Zeile und Spalte.
|
||||
fn assert_output_matches(file: &str, want: &str, got: &str) {
|
||||
// Sollausgaben werden je Checkout mit CRLF ausgeliefert; Snapshots sind LF.
|
||||
let (want, got) = (want.replace("\r\n", "\n"), got.replace("\r\n", "\n"));
|
||||
let (want, got) = (want.as_str(), got.as_str());
|
||||
if want == got {
|
||||
return;
|
||||
}
|
||||
@@ -688,8 +691,8 @@ fn tbc_isam_open_in_transaktion_hat_eine_harte_frist() {
|
||||
);
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
include_str!("../../../tests/compat/isamopen.out")
|
||||
String::from_utf8_lossy(&out.stdout).replace("\r\n", "\n"),
|
||||
include_str!("../../../tests/compat/isamopen.out").replace("\r\n", "\n")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -409,7 +409,12 @@ pub fn prepare_publication(
|
||||
}
|
||||
};
|
||||
prepare(&temporary.0)?;
|
||||
fs::File::open(&temporary.0)?.sync_all()?;
|
||||
// Windows verlangt für FlushFileBuffers Schreibrechte; lesend geöffnet meldet sync_all
|
||||
// „Zugriff verweigert“ (os error 5). Die Datei ist soeben angelegt und bleibt schreibbar.
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&temporary.0)?
|
||||
.sync_all()?;
|
||||
ensure!(!cancelled(), "Export abgebrochen");
|
||||
Ok(PreparedPublication {
|
||||
path: path.into(),
|
||||
|
||||
@@ -291,7 +291,7 @@ pub struct App {
|
||||
pub active: u64,
|
||||
pub size: (u16, u16),
|
||||
pub menu: Option<(usize, usize)>,
|
||||
pub control_menu: bool,
|
||||
pub control_menu: Option<usize>,
|
||||
pub dialog: Option<Dialog>,
|
||||
pub properties: bool,
|
||||
pub value_focus: bool,
|
||||
@@ -339,7 +339,7 @@ impl App {
|
||||
active: 0,
|
||||
size,
|
||||
menu: None,
|
||||
control_menu: false,
|
||||
control_menu: None,
|
||||
dialog: None,
|
||||
properties: false,
|
||||
value_focus: false,
|
||||
@@ -401,6 +401,30 @@ impl App {
|
||||
WindowState::Normal => clamp(w.normal, area),
|
||||
}
|
||||
}
|
||||
/// Rechteck des Control-Menü-Popups, für alle Fensterarten nach derselben Regel: linke
|
||||
/// Spalte gleich Spalte des `[≡]`-Symbols, bevorzugt direkt unter der Titelzeile; passt es
|
||||
/// dort nicht, klappt es nach oben und endet direkt über der Titelzeile; passt es beidseitig
|
||||
/// nicht, gewinnt die Seite mit mehr Platz. Rechts wird nur so weit nach links gerückt, dass
|
||||
/// es in die Arbeitsfläche passt und die Symbolspalte überdeckt bleibt.
|
||||
pub fn control_menu_rect(&self, width: u16, height: u16) -> Rect {
|
||||
let area = self.area();
|
||||
let anchor = self.active_window().map(|w| self.rect(w)).unwrap_or(area);
|
||||
let below = area.bottom().saturating_sub(anchor.y + 1);
|
||||
let above = anchor.y.saturating_sub(area.y);
|
||||
let (y, height) = if height <= below {
|
||||
(anchor.y + 1, height)
|
||||
} else if height <= above {
|
||||
(anchor.y - height, height)
|
||||
} else if below >= above {
|
||||
(anchor.y + 1, below.max(3))
|
||||
} else {
|
||||
let h = above.max(3);
|
||||
(anchor.y.saturating_sub(h), h)
|
||||
};
|
||||
let width = width.min(area.width);
|
||||
let x = anchor.x.min(area.right().saturating_sub(width));
|
||||
Rect::new(x, y, width, height)
|
||||
}
|
||||
fn add_window(&mut self, kind: WindowKind, normal: Rect) -> u64 {
|
||||
let id = self.next_window;
|
||||
self.next_window += 1;
|
||||
@@ -569,8 +593,7 @@ impl App {
|
||||
self.message = reason;
|
||||
return;
|
||||
}
|
||||
self.menu = None;
|
||||
self.control_menu = false;
|
||||
self.close_menus();
|
||||
if let Err(e) = self.action(command) {
|
||||
self.message = format!("{e:#}");
|
||||
if self.mode == Mode::Designer {
|
||||
@@ -792,8 +815,8 @@ impl App {
|
||||
}
|
||||
}
|
||||
ControlMenu => {
|
||||
self.control_menu = true;
|
||||
self.menu = Some((0, 0));
|
||||
// Fensterlokal: kein Menüleistentitel wird ausgewählt.
|
||||
self.control_menu = Some(0);
|
||||
}
|
||||
FocusWindow(id) => {
|
||||
if self.windows.iter().any(|w| w.id == id) {
|
||||
@@ -1539,8 +1562,8 @@ impl App {
|
||||
&& !self.program_focus()
|
||||
&& self.dialog.is_none()
|
||||
{
|
||||
if let Some((m, i)) = self.menu {
|
||||
if let Some((_, Some(command))) = self.menu_entries(m).get(i) {
|
||||
if let Some((entries, i)) = self.open_entries() {
|
||||
if let Some((_, Some(command))) = entries.get(i) {
|
||||
self.help_menu_context(*command);
|
||||
}
|
||||
} else {
|
||||
@@ -1549,7 +1572,7 @@ impl App {
|
||||
return;
|
||||
}
|
||||
if self.dialog.is_none()
|
||||
&& self.menu.is_none()
|
||||
&& !self.menu_open()
|
||||
&& self
|
||||
.active_window()
|
||||
.is_some_and(|w| w.kind == WindowKind::Help)
|
||||
@@ -1581,7 +1604,7 @@ impl App {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if self.dialog.is_none() && self.menu.is_none() && self.program_focus() {
|
||||
if self.dialog.is_none() && !self.menu_open() && self.program_focus() {
|
||||
if self.session.fullscreen {
|
||||
self.basic_events.push(Event::Mouse(mouse));
|
||||
return;
|
||||
@@ -1604,7 +1627,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
if mouse.kind == MouseEventKind::Down(MouseButton::Right) {
|
||||
if self.dialog.is_none() && self.menu.is_none() && self.options.right_help {
|
||||
if self.dialog.is_none() && !self.menu_open() && self.options.right_help {
|
||||
self.execute(Command::Topic);
|
||||
}
|
||||
return;
|
||||
@@ -1619,7 +1642,7 @@ impl App {
|
||||
if let Some(hit) = hit {
|
||||
self.click(hit);
|
||||
}
|
||||
} else if self.dialog.is_none() && self.menu.is_none() {
|
||||
} else if self.dialog.is_none() && !self.menu_open() {
|
||||
if let Some(Window {
|
||||
kind: WindowKind::Code(v),
|
||||
..
|
||||
@@ -1641,7 +1664,7 @@ impl App {
|
||||
self.basic_events.push(event);
|
||||
} else if let Event::Paste(text) = event {
|
||||
if self.dialog.is_none()
|
||||
&& self.menu.is_none()
|
||||
&& !self.menu_open()
|
||||
&& self
|
||||
.active_window()
|
||||
.is_some_and(|w| w.kind == WindowKind::Immediate)
|
||||
@@ -1649,7 +1672,7 @@ impl App {
|
||||
self.debugger.immediate.push_str(&text);
|
||||
return;
|
||||
}
|
||||
if self.dialog.is_none() && self.menu.is_none() && self.mode == Mode::Environment {
|
||||
if self.dialog.is_none() && !self.menu_open() && self.mode == Mode::Environment {
|
||||
if let Err(e) = self.editor_insert(&text) {
|
||||
self.message = e.to_string();
|
||||
}
|
||||
@@ -1676,19 +1699,17 @@ impl App {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.menu.is_some() {
|
||||
if self.menu_open() {
|
||||
match hit {
|
||||
Hit::Menu(i) => self.menu = Some((i, 0)),
|
||||
Hit::Menu(i) => {
|
||||
self.control_menu = None;
|
||||
self.menu = Some((i, 0));
|
||||
}
|
||||
Hit::MenuItem(i) => {
|
||||
if let Some((menu, _)) = self.menu {
|
||||
self.menu = Some((menu, i));
|
||||
self.menu_enter();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.menu = None;
|
||||
self.control_menu = false;
|
||||
self.select_open(i);
|
||||
self.menu_enter();
|
||||
}
|
||||
_ => self.close_menus(),
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1763,14 +1784,14 @@ impl App {
|
||||
fn key(&mut self, key: KeyEvent) {
|
||||
if self.dialog.is_none() && key.code == K::F(1) {
|
||||
if key.modifiers.is_empty() {
|
||||
if let Some((m, i)) = self.menu {
|
||||
if let Some((_, Some(command))) = self.menu_entries(m).get(i) {
|
||||
if let Some((entries, i)) = self.open_entries() {
|
||||
if let Some((_, Some(command))) = entries.get(i) {
|
||||
self.help_menu_context(*command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.menu.is_none() {
|
||||
if !self.menu_open() {
|
||||
if !self.help_key(key) {
|
||||
self.execute(if key.modifiers.contains(M::SHIFT) {
|
||||
Command::UsingHelp
|
||||
@@ -1787,7 +1808,7 @@ impl App {
|
||||
}
|
||||
if self.editor.chord.is_some()
|
||||
&& self.dialog.is_none()
|
||||
&& self.menu.is_none()
|
||||
&& !self.menu_open()
|
||||
&& !self.program_focus()
|
||||
{
|
||||
if let Err(e) = self.editor_key(key) {
|
||||
@@ -1828,7 +1849,7 @@ impl App {
|
||||
self.execute(Command::MenuBar);
|
||||
return;
|
||||
}
|
||||
if self.menu.is_some() {
|
||||
if self.menu_open() {
|
||||
if key.modifiers.contains(M::ALT) {
|
||||
if let K::Char(c) = key.code {
|
||||
if let Some(i) = self
|
||||
@@ -1836,45 +1857,46 @@ impl App {
|
||||
.iter()
|
||||
.position(|m| m.mnemonic == c.to_ascii_lowercase())
|
||||
{
|
||||
self.control_menu = false;
|
||||
self.control_menu = None;
|
||||
self.menu = Some((i, 0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
match key.code {
|
||||
K::Esc => {
|
||||
self.menu = None;
|
||||
self.control_menu = false;
|
||||
}
|
||||
K::Left | K::Right if !self.control_menu => {
|
||||
let n = self.menus().len();
|
||||
let (m, _) = self.menu.unwrap();
|
||||
self.menu = Some((
|
||||
(m + n + if key.code == K::Right { 1 } else { n - 1 }) % n,
|
||||
0,
|
||||
));
|
||||
K::Esc => self.close_menus(),
|
||||
K::Left | K::Right => {
|
||||
// Nur die Menüleiste wechselt seitlich; das Control-Menü bleibt stehen.
|
||||
if let Some((m, _)) = self.menu {
|
||||
let n = self.menus().len();
|
||||
self.menu = Some((
|
||||
(m + n + if key.code == K::Right { 1 } else { n - 1 }) % n,
|
||||
0,
|
||||
));
|
||||
}
|
||||
}
|
||||
K::Up | K::Down => {
|
||||
let (m, i) = self.menu.unwrap();
|
||||
let n = self.menu_entries(m).len();
|
||||
if n > 0 {
|
||||
self.menu =
|
||||
Some((m, (i + n + if key.code == K::Down { 1 } else { n - 1 }) % n));
|
||||
if let Some((entries, i)) = self.open_entries() {
|
||||
let n = entries.len();
|
||||
if n > 0 {
|
||||
self.select_open(
|
||||
(i + n + if key.code == K::Down { 1 } else { n - 1 }) % n,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
K::Enter => self.menu_enter(),
|
||||
K::Char(c) => {
|
||||
let (m, _) = self.menu.unwrap();
|
||||
let entries = self.menu_entries(m);
|
||||
if let Some(i) = entries.iter().position(|(label, _)| {
|
||||
label
|
||||
.split_once('&')
|
||||
.and_then(|(_, s)| s.chars().next())
|
||||
.is_some_and(|v| v.eq_ignore_ascii_case(&c))
|
||||
}) {
|
||||
self.menu = Some((m, i));
|
||||
self.menu_enter();
|
||||
if let Some((entries, _)) = self.open_entries() {
|
||||
if let Some(i) = entries.iter().position(|(label, _)| {
|
||||
label
|
||||
.split_once('&')
|
||||
.and_then(|(_, s)| s.chars().next())
|
||||
.is_some_and(|v| v.eq_ignore_ascii_case(&c))
|
||||
}) {
|
||||
self.select_open(i);
|
||||
self.menu_enter();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -2123,17 +2145,40 @@ impl App {
|
||||
}
|
||||
self.editor_key(key)
|
||||
}
|
||||
pub fn menu_entries(&self, index: usize) -> Vec<(String, Option<Command>)> {
|
||||
if self.control_menu {
|
||||
return vec![
|
||||
("&Restore".into(), Some(Command::Restore)),
|
||||
("&Move".into(), Some(Command::MoveWindow)),
|
||||
("&Size".into(), Some(Command::SizeWindow)),
|
||||
("Mi&nimize".into(), Some(Command::Minimize)),
|
||||
("Ma&ximize".into(), Some(Command::Maximize)),
|
||||
("&Close".into(), Some(Command::CloseWindow)),
|
||||
];
|
||||
/// Einträge des fensterlokalen Control-Menüs, für alle Fensterarten gleich.
|
||||
pub fn control_entries(&self) -> MenuEntries {
|
||||
vec![
|
||||
("&Restore".into(), Some(Command::Restore)),
|
||||
("&Move".into(), Some(Command::MoveWindow)),
|
||||
("&Size".into(), Some(Command::SizeWindow)),
|
||||
("Mi&nimize".into(), Some(Command::Minimize)),
|
||||
("Ma&ximize".into(), Some(Command::Maximize)),
|
||||
("&Close".into(), Some(Command::CloseWindow)),
|
||||
]
|
||||
}
|
||||
/// Ist ein Menü (Menüleiste oder Control-Menü) geöffnet?
|
||||
pub fn menu_open(&self) -> bool {
|
||||
self.menu.is_some() || self.control_menu.is_some()
|
||||
}
|
||||
/// Einträge und Auswahl der gerade offenen Liste: Control-Menü hat Vorrang vor der Menüleiste.
|
||||
pub fn open_entries(&self) -> Option<(MenuEntries, usize)> {
|
||||
if let Some(i) = self.control_menu {
|
||||
return Some((self.control_entries(), i));
|
||||
}
|
||||
self.menu.map(|(m, i)| (self.menu_entries(m), i))
|
||||
}
|
||||
fn select_open(&mut self, i: usize) {
|
||||
if self.control_menu.is_some() {
|
||||
self.control_menu = Some(i);
|
||||
} else if let Some((m, _)) = self.menu {
|
||||
self.menu = Some((m, i));
|
||||
}
|
||||
}
|
||||
pub(crate) fn close_menus(&mut self) {
|
||||
self.menu = None;
|
||||
self.control_menu = None;
|
||||
}
|
||||
pub fn menu_entries(&self, index: usize) -> MenuEntries {
|
||||
let menus = self.menus();
|
||||
let mut entries = menus[index]
|
||||
.items
|
||||
@@ -2151,13 +2196,15 @@ impl App {
|
||||
entries
|
||||
}
|
||||
fn menu_enter(&mut self) {
|
||||
if let Some((m, i)) = self.menu {
|
||||
if let Some((_, Some(c))) = self.menu_entries(m).get(i) {
|
||||
if let Some((entries, i)) = self.open_entries() {
|
||||
if let Some((_, Some(c))) = entries.get(i) {
|
||||
self.execute(*c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Beschriftungen und Befehle einer aufgeklappten Menüliste.
|
||||
pub type MenuEntries = Vec<(String, Option<Command>)>;
|
||||
pub fn clamp(r: Rect, area: Rect) -> Rect {
|
||||
let width = r.width.max(12).min(area.width);
|
||||
let height = r.height.max(4).min(area.height);
|
||||
|
||||
@@ -1481,7 +1481,7 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn design_mouse(&mut self, m: MouseEvent) -> Result<bool> {
|
||||
if self.mode != Mode::Designer || self.dialog.is_some() || self.menu.is_some() {
|
||||
if self.mode != Mode::Designer || self.dialog.is_some() || self.menu_open() {
|
||||
return Ok(false);
|
||||
}
|
||||
if let Some((property, color)) = self.designer.paint {
|
||||
|
||||
@@ -796,8 +796,7 @@ impl App {
|
||||
}
|
||||
pub(crate) fn help_menu_context(&mut self, command: Command) {
|
||||
let target = command_target(command, self.mode == Mode::Designer);
|
||||
self.menu = None;
|
||||
self.control_menu = false;
|
||||
self.close_menus();
|
||||
self.help_show();
|
||||
match Catalog::embedded().resolve("", target) {
|
||||
Ok((p, b)) => self.help.visit(p, b, None),
|
||||
|
||||
@@ -64,7 +64,7 @@ impl App {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if self.session.fullscreen && self.dialog.is_none() && self.menu.is_none() {
|
||||
if self.session.fullscreen && self.dialog.is_none() && !self.menu_open() {
|
||||
f.render_widget(tb_ui::screen::ScreenWidget(self.session.screen()), area);
|
||||
return;
|
||||
}
|
||||
@@ -317,7 +317,7 @@ impl App {
|
||||
if active
|
||||
&& expansion.is_none()
|
||||
&& self.dialog.is_none()
|
||||
&& self.menu.is_none()
|
||||
&& !self.menu_open()
|
||||
{
|
||||
let before = &doc.code()[..v.cursor.min(doc.code().len())];
|
||||
let row = before.bytes().filter(|c| *c == b'\n').count();
|
||||
@@ -523,26 +523,30 @@ impl App {
|
||||
x += r.width;
|
||||
}
|
||||
}
|
||||
if let Some((menu, selected)) = self.menu {
|
||||
let entries = self.menu_entries(menu);
|
||||
if let Some((entries, selected)) = self.open_entries() {
|
||||
let width = (entries.iter().map(|(s, _)| s.width()).max().unwrap_or(12) as u16 + 4)
|
||||
.min(area.width);
|
||||
let menus = self.menus();
|
||||
let x = if self.control_menu {
|
||||
self.active_window().map(|w| self.rect(w).x).unwrap_or(0)
|
||||
} else if menus[menu].title == "Help" {
|
||||
area.width - width
|
||||
// Control-Menü hängt am Fenstersymbol, Menüleistenmenüs unter ihrem Titel.
|
||||
let (x, y, visible) = if self.control_menu.is_some() {
|
||||
let r = self.control_menu_rect(width, entries.len() as u16 + 2);
|
||||
(r.x, r.y, r.height.saturating_sub(2).max(1) as usize)
|
||||
} else {
|
||||
menus
|
||||
.iter()
|
||||
.take(menu)
|
||||
.map(|m| m.title.len() as u16 + 2)
|
||||
.sum::<u16>()
|
||||
.min(area.width - width)
|
||||
let menu = self.menu.map(|(m, _)| m).unwrap_or(0);
|
||||
let menus = self.menus();
|
||||
let x = if menus[menu].title == "Help" {
|
||||
area.width - width
|
||||
} else {
|
||||
menus
|
||||
.iter()
|
||||
.take(menu)
|
||||
.map(|m| m.title.len() as u16 + 2)
|
||||
.sum::<u16>()
|
||||
.min(area.width - width)
|
||||
};
|
||||
(x, 1, (area.height - 4) as usize)
|
||||
};
|
||||
let visible = (area.height - 4) as usize;
|
||||
let start = selected.saturating_sub(visible - 1);
|
||||
let rect = Rect::new(x, 1, width, (entries.len().min(visible) + 2) as u16);
|
||||
let rect = Rect::new(x, y, width, (entries.len().min(visible) + 2) as u16);
|
||||
f.render_widget(Clear, rect);
|
||||
f.render_widget(Block::bordered().style(style(self, 0)), rect);
|
||||
for (i, (text, c)) in entries.iter().enumerate().skip(start).take(visible) {
|
||||
@@ -554,7 +558,7 @@ impl App {
|
||||
} else {
|
||||
style(self, 0)
|
||||
};
|
||||
let r = Rect::new(x + 1, 2 + (i - start) as u16, width - 2, 1);
|
||||
let r = Rect::new(x + 1, y + 1 + (i - start) as u16, width - 2, 1);
|
||||
let label = if disabled.is_some() {
|
||||
format!("× {text}")
|
||||
} else if *c == Some(Command::SyntaxChecking) && self.options.syntax_checking {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -3243,6 +3243,8 @@ End
|
||||
m.set(8, selected, PropertyValue::Integer(0)).unwrap();
|
||||
m.object_method(11, "PRINT", vec![PropertyValue::String("Bild".into())])
|
||||
.unwrap();
|
||||
// Die DriveListBox zeigt den Wurzeltrenner der Plattform (`/` bzw. `\`).
|
||||
let drive = format!("{} ▼ ", std::path::MAIN_SEPARATOR);
|
||||
let snapshots = [
|
||||
(1, "<OK> "),
|
||||
(2, "Text "),
|
||||
@@ -3256,7 +3258,7 @@ End
|
||||
(10, "▲"),
|
||||
(11, "┌──────┐"),
|
||||
(13, "┌──────┐"),
|
||||
(14, "/ ▼ "),
|
||||
(14, drive.as_str()),
|
||||
(15, "┌──────┐"),
|
||||
(18, "▲"),
|
||||
];
|
||||
|
||||
@@ -240,19 +240,23 @@ pub fn relative_path(base: &Path, target: &Path) -> std::io::Result<String> {
|
||||
)
|
||||
});
|
||||
}
|
||||
let mut result = PathBuf::new();
|
||||
// Relative Verweise werden plattformneutral mit `/` geschrieben, damit ein unter
|
||||
// Windows gespeichertes Projekt auch unter Linux und macOS lädt; beide Plattformen
|
||||
// lösen `/` beim Laden auf.
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
for _ in common..a.len() {
|
||||
result.push("..");
|
||||
parts.push("..".into());
|
||||
}
|
||||
for c in &b[common..] {
|
||||
result.push(c.as_os_str());
|
||||
let s = c.as_os_str().to_str().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Projektpfad ist nicht UTF-8",
|
||||
)
|
||||
})?;
|
||||
parts.push(s.to_owned());
|
||||
}
|
||||
result.to_str().map(str::to_owned).ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Projektpfad ist nicht UTF-8",
|
||||
)
|
||||
})
|
||||
Ok(parts.join("/"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -162,7 +162,8 @@ fn documented_mak_example_is_accepted_by_the_shared_loader() {
|
||||
t.write("main.bas", "END\n");
|
||||
t.write("lib.bas", "");
|
||||
t.write("form.frm", "VERSION 1.00\nBegin Form F\nEnd\n");
|
||||
let docs = include_str!("../../../docs/dateiformate.md");
|
||||
// Die Dokumentation liegt je Checkout mit CRLF vor; der Beispielblock wird LF-neutral gesucht.
|
||||
let docs = include_str!("../../../docs/dateiformate.md").replace("\r\n", "\n");
|
||||
let example = docs
|
||||
.split("```mak\n")
|
||||
.nth(1)
|
||||
|
||||
Reference in New Issue
Block a user