Phase 6: Plattformprüfungen und absolute RGB-Farben, Change 05 archivieren

This commit is contained in:
2026-09-07 21:12:25 +02:00
parent 5aa920b768
commit 796564795a
55 changed files with 3772 additions and 200 deletions

View File

@@ -11,74 +11,9 @@ use ratatui::{
};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
/// DOS-Farben der IDE. Die ersten 16 ANSI-Einträge gehören dem Terminalprofil.
/// BASIC-Ausgabe verwendet weiterhin ihre eigene Farbabbildung in tb-ui.
/// Feste DOS-RGB-Farben, identisch für IDE und BASIC-Ausgabe.
pub fn dos(n: u8) -> Color {
static COLORS: std::sync::OnceLock<u16> = std::sync::OnceLock::new();
dos_color(
n,
*COLORS.get_or_init(|| {
let detected = crossterm::style::available_color_count();
// Ein generisches COLORTERM (z.B. "yes") verdeckt bei crossterm TERM.
if detected < 256 && std::env::var("TERM").is_ok_and(|s| s.contains("256")) {
256
} else {
detected
}
}),
)
}
pub fn dos_color(n: u8, colors: u16) -> Color {
let n = n.min(15) as usize;
const RGB: [(u8, u8, u8); 16] = [
(0, 0, 0),
(0, 0, 170),
(0, 170, 0),
(0, 170, 170),
(170, 0, 0),
(170, 0, 170),
(170, 85, 0),
(170, 170, 170),
(85, 85, 85),
(85, 85, 255),
(85, 255, 85),
(85, 255, 255),
(255, 85, 85),
(255, 85, 255),
(255, 255, 85),
(255, 255, 255),
];
if colors > 256 {
let (r, g, b) = RGB[n];
Color::Rgb(r, g, b)
} else if colors >= 256 {
// Fester 6x6x6-Farbwürfel / Graurampe statt profilabhängiger Systemfarben.
Color::Indexed(
[
16, 19, 34, 37, 124, 127, 130, 248, 240, 63, 83, 87, 203, 207, 227, 231,
][n],
)
} else {
[
Color::Black,
Color::Blue,
Color::Green,
Color::Cyan,
Color::Red,
Color::Magenta,
Color::Yellow,
Color::Gray,
Color::DarkGray,
Color::LightBlue,
Color::LightGreen,
Color::LightCyan,
Color::LightRed,
Color::LightMagenta,
Color::LightYellow,
Color::White,
][n]
}
tb_ui::screen::basic_color(n.min(15))
}
pub(crate) fn pair(fg: u8, bg: u8) -> Style {
@@ -160,6 +95,8 @@ impl App {
5
},
);
// Block/Paragraph ändern Stile, löschen aber keine vorhandenen Zeichen.
f.render_widget(Clear, rect);
f.render_widget(
Block::default()
.borders(Borders::ALL)

View File

@@ -930,21 +930,7 @@ fn theme_rgb(color: ratatui::style::Color) -> (u8, u8, u8) {
use ratatui::style::Color;
match color {
Color::Rgb(r, g, b) => (r, g, b),
Color::Indexed(n @ 16..=231) => {
let n = n - 16;
let channel = |v: u8| if v == 0 { 0 } else { 55 + v * 40 };
(channel(n / 36), channel(n / 6 % 6), channel(n % 6))
}
Color::Indexed(n @ 232..=255) => {
let v = 8 + (n - 232) * 10;
(v, v, v)
}
_ => {
let n = (0..16)
.find(|n| tb_ide::render::dos_color(*n, 16) == color)
.unwrap_or_else(|| panic!("Unbestimmte IDE-Farbe: {color:?}"));
theme_rgb(tb_ide::render::dos_color(n, u16::MAX))
}
_ => panic!("Keine absolute RGB-Farbe: {color:?}"),
}
}
fn theme_contrast(fg: ratatui::style::Color, bg: ratatui::style::Color) -> f64 {
@@ -983,23 +969,21 @@ fn assert_theme(app: &mut App, context: &str) {
#[test]
fn theme_contrast_covers_rendered_states_and_rejects_bad_pairs() {
use tb_ide::render::dos_color;
for colors in [256, u16::MAX] {
for (fg, bg) in tb_ide::options::Options::default().colors {
assert!(theme_contrast(dos_color(fg, colors), dos_color(bg, colors)) >= 4.5);
}
assert!(theme_contrast(dos_color(15, colors), dos_color(7, colors)) < 4.5);
for n in 0..16 {
let fg = if matches!(n, 0 | 1 | 4 | 5 | 6 | 8 | 9) {
15
} else {
0
};
assert!(
theme_contrast(dos_color(fg, colors), dos_color(n, colors)) >= 4.5,
"Palettenbeschriftung {n}"
);
}
use tb_ide::render::dos;
for (fg, bg) in tb_ide::options::Options::default().colors {
assert!(theme_contrast(dos(fg), dos(bg)) >= 4.5);
}
assert!(theme_contrast(dos(15), dos(7)) < 4.5);
for n in 0..16 {
let fg = if matches!(n, 0 | 1 | 4 | 5 | 6 | 8 | 9) {
15
} else {
0
};
assert!(
theme_contrast(dos(fg), dos(n)) >= 4.5,
"Palettenbeschriftung {n}"
);
}
let t = Temp::new();
let mut app = t.app();
@@ -1086,34 +1070,23 @@ fn theme_color_commands_use_explicit_values_and_never_modify_terminal_palette()
buffer::Cell,
style::Color,
};
use tb_ide::render::dos_color;
assert_eq!(dos_color(1, u16::MAX), Color::Rgb(0, 0, 170));
assert_eq!(dos_color(5, u16::MAX), Color::Rgb(170, 0, 170));
assert_eq!(dos_color(6, u16::MAX), Color::Rgb(170, 85, 0));
for colors in [16, 256, u16::MAX] {
for n in 0..16 {
let color = dos_color(n, colors);
if let Color::Indexed(index) = color {
assert!(index >= 16);
}
let mut cell = Cell::default();
cell.set_symbol("X")
.set_bg(color)
.set_fg(dos_color(15, colors));
let mut output = Vec::new();
CrosstermBackend::new(&mut output)
.draw(std::iter::once((0, 0, &cell)))
.unwrap();
let sequence = String::from_utf8(output).unwrap();
assert!(sequence.contains('X'));
if colors >= 256 {
assert!(sequence.contains("48;"));
}
if colors > 256 {
assert!(sequence.contains("48;2;"));
}
assert!(!sequence.contains("\x1b]")); // Keine globale Palettenänderung.
}
use tb_ide::render::dos;
assert_eq!(dos(1), Color::Rgb(0, 0, 170));
assert_eq!(dos(5), Color::Rgb(170, 0, 170));
assert_eq!(dos(6), Color::Rgb(170, 85, 0));
for n in 0..16 {
assert_eq!(dos(n), tb_ui::screen::basic_color(n));
let mut cell = Cell::default();
cell.set_symbol("X").set_bg(dos(n)).set_fg(dos(15));
let mut output = Vec::new();
CrosstermBackend::new(&mut output)
.draw(std::iter::once((0, 0, &cell)))
.unwrap();
let sequence = String::from_utf8(output).unwrap();
assert!(sequence.contains('X'));
assert!(sequence.contains("38;2;255;255;255"));
assert!(sequence.contains("48;2;"));
assert!(!sequence.contains("\x1b]")); // Keine globale Palettenänderung.
}
}
@@ -1149,3 +1122,61 @@ fn theme_designer_chrome_and_handles_preserve_program_palette() {
assert_theme(&mut app, "Designer-Werkzeug");
}
}
#[test]
fn theme_windows_erase_desktop_and_underlying_windows_on_every_redraw() {
use ratatui::{
layout::Rect,
style::{Color, Modifier},
};
let t = Temp::new();
let mut app = t.app();
app.options.syntax_checking = false;
// Ein sichtbares Zeichen macht das Durchscheinen sicher erkennbar.
app.options.desktop = '@';
type_text(&mut app, "PRINT 42");
let mut term = Terminal::new(TestBackend::new(app.size.0, app.size.1)).unwrap();
for selected in [false, true, false] {
if selected {
key(&mut app, K::Char('a'), M::CONTROL);
} else {
plain(&mut app, K::Right);
}
term.draw(|f| app.render(f)).unwrap();
let b = term.backend().buffer();
let rect = app.rect(app.windows.iter().find(|w| w.id == app.active).unwrap());
let inner = Rect::new(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2);
for y in inner.y..inner.bottom() {
for x in inner.x..inner.right() {
let c = &b[(x, y)];
assert_ne!(c.symbol(), "@", "Desktop in Fenster bei {x}/{y}");
assert_eq!(c.bg, Color::Rgb(0, 0, 170));
if y > inner.y {
assert_eq!(c.symbol(), " ", "Restinhalt bei {x}/{y}");
assert!(!c.modifier.contains(Modifier::REVERSED));
}
}
}
assert_eq!(b[(inner.x, inner.y)].symbol(), "P");
assert_eq!(
b[(inner.x, inner.y)].modifier.contains(Modifier::REVERSED),
selected
);
}
// Beim Aktivieren eines überlappenden Fensters müssen auch fremde Texte weg.
app.execute(Command::NewModule);
plain(&mut app, K::Enter);
assert!(app.dialog.is_none());
term.draw(|f| app.render(f)).unwrap();
let b = term.backend().buffer();
let rect = app.rect(app.windows.iter().find(|w| w.id == app.active).unwrap());
for y in rect.y + 1..rect.bottom() - 1 {
for x in rect.x + 1..rect.right() - 1 {
assert_eq!(
b[(x, y)].symbol(),
" ",
"Überdeckter Fensterinhalt bei {x}/{y}"
);
}
}
}

View File

@@ -530,3 +530,35 @@ fn comment_only_source_change_also_requires_revision_choice() {
DialogKind::ResumeRevision
));
}
#[test]
fn platform_probe_records_each_basic_key_once_and_keeps_ide_keys_out() {
let t = Temp::new();
let log = t.0.join("platform-events.log");
let source = include_str!("../../../tests/platform/input.frm")
.replace("platform-events.log", &log.to_string_lossy());
let path = t.0.join("input.frm");
fs::write(&path, source).unwrap();
let mut a = t.app("");
a.load_initial_project(path).unwrap();
a.execute(Command::Start);
tick(&mut a);
key(&mut a, K::Char('a'), M::NONE);
tick(&mut a);
a.handle(Event::Key(KeyEvent::new_with_kind(
K::Char('a'),
M::NONE,
crossterm::event::KeyEventKind::Release,
)));
tick(&mut a);
let events = fs::read_to_string(&log).unwrap();
assert_eq!(
events.replace('\r', ""),
"Down: 97 : 0 \nPress: 97 : 0 \nUp: 97 : 0 \n"
);
edit_window(&mut a);
key(&mut a, K::F(1), M::NONE);
tick(&mut a);
assert_eq!(fs::read_to_string(&log).unwrap(), events);
assert!(matches!(a.active_window().unwrap().kind, WindowKind::Help));
}