Phase 6: VBDOS-Theme umsetzen und Change 05a archivieren
This commit is contained in:
@@ -7,12 +7,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{anyhow, ensure, Result};
|
||||
use crossterm::event::{KeyCode as K, KeyEvent, KeyModifiers as M};
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
use ratatui::{layout::Rect, widgets::Paragraph, Frame};
|
||||
use tb_frontend::{Diagnostic, SourcePos};
|
||||
use tb_vm::interp::{DebugLocation, DebugWatch, Vm};
|
||||
|
||||
@@ -682,8 +677,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
f.render_widget(
|
||||
Paragraph::new(lines.join("\n"))
|
||||
.style(Style::default().fg(Color::White).bg(Color::Blue)),
|
||||
Paragraph::new(lines.join("\n")).style(crate::render::pair(15, 1)),
|
||||
r,
|
||||
);
|
||||
if kind == WindowKind::Immediate && r.height > 4 {
|
||||
|
||||
@@ -1702,7 +1702,7 @@ impl App {
|
||||
let r = Rect::new(area.x + timers * 4, area.bottom().saturating_sub(1), 3, 1);
|
||||
timers += 1;
|
||||
f.render_widget(
|
||||
Paragraph::new("[T]").style(Style::default().fg(crate::render::dos(14))),
|
||||
Paragraph::new("[T]").style(crate::render::pair(0, 14)),
|
||||
r.intersection(area),
|
||||
);
|
||||
r
|
||||
@@ -1749,7 +1749,7 @@ impl App {
|
||||
(r.right() - 1, r.y + r.height / 2),
|
||||
] {
|
||||
f.render_widget(
|
||||
Paragraph::new("■").style(Style::default().fg(crate::render::dos(14))),
|
||||
Paragraph::new("■").style(crate::render::pair(0, 14)),
|
||||
Rect::new(x, y, 1, 1),
|
||||
);
|
||||
}
|
||||
@@ -1789,9 +1789,15 @@ impl App {
|
||||
if !r.is_empty() {
|
||||
f.render_widget(
|
||||
Paragraph::new(format!("{n:X} ")).style(
|
||||
Style::default()
|
||||
.bg(crate::render::dos(n as u8))
|
||||
.fg(crate::render::dos(if n < 8 { 15 } else { 0 })),
|
||||
Style::default().bg(crate::render::dos(n as u8)).fg(
|
||||
crate::render::dos(
|
||||
if matches!(n, 0 | 1 | 4 | 5 | 6 | 8 | 9) {
|
||||
15
|
||||
} else {
|
||||
0
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
r,
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ use crossterm::event::{KeyCode as K, KeyEvent, KeyModifiers as M};
|
||||
use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
@@ -165,7 +165,7 @@ impl Page {
|
||||
style = style.add_modifier(Modifier::ITALIC);
|
||||
}
|
||||
if heading.is_some() {
|
||||
style = style.fg(Color::Yellow);
|
||||
style = style.fg(crate::render::dos(1));
|
||||
}
|
||||
match event {
|
||||
Event::Start(Tag::Heading { level, .. }) => {
|
||||
@@ -864,7 +864,7 @@ impl App {
|
||||
let start = self.help.row(&rows);
|
||||
f.render_widget(
|
||||
Paragraph::new(format!("{} · Suche: {}", page.title, self.help.input))
|
||||
.style(Style::default().fg(Color::Yellow)),
|
||||
.style(Style::default().fg(crate::render::dos(1))),
|
||||
Rect::new(area.x, area.y, area.width, 1),
|
||||
);
|
||||
for (y, row) in rows
|
||||
@@ -887,9 +887,11 @@ impl App {
|
||||
if x >= shift && end <= shift + area.width as usize {
|
||||
let mut style = g.style;
|
||||
if let Some(link) = g.link {
|
||||
style = style.fg(Color::Cyan).add_modifier(Modifier::UNDERLINED);
|
||||
style = style
|
||||
.fg(crate::render::dos(1))
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
if self.help.position.link == Some(link) {
|
||||
style = style.bg(Color::Blue).fg(Color::White);
|
||||
style = style.bg(crate::render::dos(1)).fg(crate::render::dos(15));
|
||||
}
|
||||
}
|
||||
if let Some(last) = spans.last_mut().filter(|s| s.style == style) {
|
||||
|
||||
@@ -11,29 +11,82 @@ 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.
|
||||
pub fn dos(n: u8) -> Color {
|
||||
[
|
||||
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.min(15) as usize]
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pair(fg: u8, bg: u8) -> Style {
|
||||
Style::default().fg(dos(fg)).bg(dos(bg))
|
||||
}
|
||||
fn style(app: &App, element: usize) -> Style {
|
||||
let (fg, bg) = app.options.colors[element];
|
||||
Style::default().fg(dos(fg)).bg(dos(bg))
|
||||
pair(fg, bg)
|
||||
}
|
||||
fn put(f: &mut Frame, area: Rect, text: impl Into<String>, style: Style) {
|
||||
f.render_widget(Paragraph::new(text.into()).style(style), area);
|
||||
@@ -72,7 +125,7 @@ impl App {
|
||||
f,
|
||||
area,
|
||||
"Terminal Basic benötigt mindestens 80×25 Zellen.",
|
||||
Style::default(),
|
||||
pair(7, 0),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -101,6 +154,8 @@ impl App {
|
||||
self,
|
||||
if matches!(w.kind, WindowKind::Code(_)) {
|
||||
2
|
||||
} else if w.kind == WindowKind::Toolbox {
|
||||
6
|
||||
} else {
|
||||
5
|
||||
},
|
||||
@@ -108,6 +163,11 @@ impl App {
|
||||
f.render_widget(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(if active {
|
||||
ratatui::widgets::BorderType::Double
|
||||
} else {
|
||||
ratatui::widgets::BorderType::Plain
|
||||
})
|
||||
.style(content_style)
|
||||
.border_style(style(self, 4)),
|
||||
rect,
|
||||
@@ -240,7 +300,7 @@ impl App {
|
||||
1,
|
||||
),
|
||||
if mark.bound { "●" } else { "○" },
|
||||
Style::default().fg(Color::Red),
|
||||
pair(15, 4),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -290,7 +350,7 @@ impl App {
|
||||
1,
|
||||
),
|
||||
mark,
|
||||
Style::default().fg(Color::Yellow),
|
||||
pair(0, 14),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -358,12 +418,12 @@ impl App {
|
||||
Paragraph::new(text)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.border_type(ratatui::widgets::BorderType::Double)
|
||||
.border_style(if active {
|
||||
Style::default().fg(Color::White)
|
||||
.border_type(if active {
|
||||
ratatui::widgets::BorderType::Double
|
||||
} else {
|
||||
style(self, 4)
|
||||
}),
|
||||
ratatui::widgets::BorderType::Plain
|
||||
})
|
||||
.border_style(style(self, 4)),
|
||||
)
|
||||
.style(content_style),
|
||||
r,
|
||||
@@ -396,7 +456,7 @@ impl App {
|
||||
r,
|
||||
text,
|
||||
if row == self.selected_member {
|
||||
Style::default().fg(Color::White).bg(Color::Black)
|
||||
pair(15, 0)
|
||||
} else {
|
||||
content_style
|
||||
},
|
||||
@@ -474,13 +534,13 @@ impl App {
|
||||
f,
|
||||
Rect::new(0, 0, area.width - width, 1),
|
||||
left,
|
||||
style(self, 0),
|
||||
style(self, 6),
|
||||
);
|
||||
put(
|
||||
f,
|
||||
Rect::new(area.width - width, 0, width, 1),
|
||||
geometry,
|
||||
style(self, 0),
|
||||
style(self, 6),
|
||||
);
|
||||
self.hits.push((
|
||||
Rect::new(0, 0, area.width / 2, 1),
|
||||
@@ -508,14 +568,14 @@ impl App {
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
&m.title[..1],
|
||||
style(self, 0).add_modifier(Modifier::UNDERLINED),
|
||||
Style::default().add_modifier(Modifier::UNDERLINED),
|
||||
),
|
||||
Span::raw(format!("{} ", &m.title[1..])),
|
||||
];
|
||||
f.render_widget(
|
||||
Paragraph::new(Line::from(spans)).style(
|
||||
if self.menu.is_some_and(|(n, _)| n == i) {
|
||||
Style::default().fg(Color::White).bg(Color::Black)
|
||||
pair(15, 0)
|
||||
} else {
|
||||
style(self, 0)
|
||||
},
|
||||
@@ -551,14 +611,16 @@ impl App {
|
||||
for (i, (text, c)) in entries.iter().enumerate().skip(start).take(visible) {
|
||||
let disabled = c.and_then(|c| self.availability(c));
|
||||
let st = if disabled.is_some() {
|
||||
Style::default().fg(Color::DarkGray).bg(Color::Gray)
|
||||
pair(0, 7).add_modifier(Modifier::ITALIC)
|
||||
} else if i == selected {
|
||||
Style::default().fg(Color::White).bg(Color::Black)
|
||||
pair(15, 0)
|
||||
} else {
|
||||
style(self, 0)
|
||||
};
|
||||
let r = Rect::new(x + 1, 2 + (i - start) as u16, width - 2, 1);
|
||||
let label = if *c == Some(Command::SyntaxChecking) && self.options.syntax_checking {
|
||||
let label = if disabled.is_some() {
|
||||
format!("× {text}")
|
||||
} else if *c == Some(Command::SyntaxChecking) && self.options.syntax_checking {
|
||||
format!("• {text}")
|
||||
} else {
|
||||
text.clone()
|
||||
@@ -605,7 +667,13 @@ impl App {
|
||||
);
|
||||
let st = style(self, 5);
|
||||
f.render_widget(Clear, rect);
|
||||
f.render_widget(Block::bordered().title(d.title.clone()).style(st), rect);
|
||||
f.render_widget(Block::bordered().style(st), rect);
|
||||
put(
|
||||
f,
|
||||
Rect::new(rect.x, rect.y, rect.width, 1),
|
||||
d.title.clone(),
|
||||
style(self, 3),
|
||||
);
|
||||
let visible = height.saturating_sub(7) as usize;
|
||||
let start = d
|
||||
.focus
|
||||
@@ -626,11 +694,7 @@ impl App {
|
||||
};
|
||||
let label = format!("{}: {}", field.label, value);
|
||||
let selected = d.focus == i;
|
||||
let field_style = if selected {
|
||||
Style::default().fg(Color::White).bg(Color::Black)
|
||||
} else {
|
||||
st
|
||||
};
|
||||
let field_style = if selected { pair(15, 0) } else { st };
|
||||
let caret = if let FieldValue::Text(text) = &field.value {
|
||||
field.label.width() + 2 + text[..field.cursor].width()
|
||||
} else {
|
||||
@@ -671,7 +735,7 @@ impl App {
|
||||
"[OK / Enter]"
|
||||
},
|
||||
if d.focus == d.fields.len() {
|
||||
Style::default().fg(Color::White).bg(Color::Black)
|
||||
pair(15, 0)
|
||||
} else {
|
||||
st
|
||||
},
|
||||
@@ -684,7 +748,7 @@ impl App {
|
||||
f,
|
||||
Rect::new(rect.x + 36, rect.bottom() - 3, width - 38, 1),
|
||||
"[Erzeugen: Phase 6]",
|
||||
Style::default().fg(Color::DarkGray).bg(Color::Gray),
|
||||
pair(0, 7).add_modifier(Modifier::ITALIC),
|
||||
);
|
||||
}
|
||||
let mut status = d.error.clone();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crossterm::event::{
|
||||
Event, KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton, MouseEvent, MouseEventKind,
|
||||
};
|
||||
use ratatui::{backend::TestBackend, style::Color, Terminal};
|
||||
use ratatui::{backend::TestBackend, Terminal};
|
||||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
@@ -105,11 +105,11 @@ fn start_snapshot_and_reference_menus_have_all_commands() {
|
||||
&& text.contains("Copyright")
|
||||
&& text.contains("00001:001")
|
||||
);
|
||||
assert_eq!(b[(4, 1)].bg, Color::Magenta);
|
||||
assert_eq!(b[(4, 1)].fg, Color::White);
|
||||
assert_eq!(b[(4, 2)].bg, Color::Blue);
|
||||
assert_eq!(b[(1, 29)].bg, Color::Cyan);
|
||||
assert_eq!(b[(1, 0)].bg, Color::Gray);
|
||||
assert_eq!(b[(4, 1)].bg, tb_ide::render::dos(5));
|
||||
assert_eq!(b[(4, 1)].fg, tb_ide::render::dos(15));
|
||||
assert_eq!(b[(4, 2)].bg, tb_ide::render::dos(1));
|
||||
assert_eq!(b[(1, 29)].bg, tb_ide::render::dos(3));
|
||||
assert_eq!(b[(1, 0)].bg, tb_ide::render::dos(7));
|
||||
plain(&mut app, K::Char('x'));
|
||||
assert_eq!(
|
||||
app.project
|
||||
@@ -663,7 +663,7 @@ fn options_apply_persist_across_bases_and_preserve_unsaved_values_on_error() {
|
||||
plain(&mut app, K::Enter);
|
||||
assert!(app.dialog.is_none());
|
||||
let (_, b) = draw(&mut app);
|
||||
assert_eq!(b[(4, 1)].bg, Color::Yellow);
|
||||
assert_eq!(b[(4, 1)].bg, tb_ide::render::dos(6));
|
||||
assert_eq!(app.options.tab_width, 4);
|
||||
let include = t.0.join("includes");
|
||||
fs::create_dir(&include).unwrap();
|
||||
@@ -924,3 +924,228 @@ fn finalization_failure_is_reported_and_preserves_existing_output() {
|
||||
.to_string_lossy()
|
||||
.starts_with(".tb-export-")));
|
||||
}
|
||||
|
||||
// Prüft tatsächlich gerenderte Zellen, einschließlich geerbter Hintergründe.
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
fn theme_contrast(fg: ratatui::style::Color, bg: ratatui::style::Color) -> f64 {
|
||||
let luminance = |color| {
|
||||
let (r, g, b) = theme_rgb(color);
|
||||
let linear = |v: u8| {
|
||||
let v = f64::from(v) / 255.0;
|
||||
if v <= 0.04045 {
|
||||
v / 12.92
|
||||
} else {
|
||||
((v + 0.055) / 1.055).powf(2.4)
|
||||
}
|
||||
};
|
||||
0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b)
|
||||
};
|
||||
let (a, b) = (luminance(fg), luminance(bg));
|
||||
(a.max(b) + 0.05) / (a.min(b) + 0.05)
|
||||
}
|
||||
fn assert_theme(app: &mut App, context: &str) {
|
||||
let (_, b) = draw(app);
|
||||
for (i, c) in b.content.iter().enumerate() {
|
||||
if !c.symbol().trim().is_empty() {
|
||||
let ratio = theme_contrast(c.fg, c.bg);
|
||||
assert!(
|
||||
ratio >= 4.5,
|
||||
"{context} ({},{}): {:?} {:?}/{:?}: {ratio:.2}:1",
|
||||
i % app.size.0 as usize,
|
||||
i / app.size.0 as usize,
|
||||
c.symbol(),
|
||||
c.fg,
|
||||
c.bg
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
let t = Temp::new();
|
||||
let mut app = t.app();
|
||||
app.options.syntax_checking = false;
|
||||
type_text(&mut app, "PRINT 42");
|
||||
assert_theme(&mut app, "Editor");
|
||||
key(&mut app, K::Char('a'), M::CONTROL);
|
||||
assert_theme(&mut app, "Textauswahl");
|
||||
for m in 0..app.menus().len() {
|
||||
for i in 0..app.menu_entries(m).len() {
|
||||
app.menu = Some((m, i));
|
||||
assert_theme(&mut app, "Menü einschließlich deaktivierter Einträge");
|
||||
}
|
||||
}
|
||||
app.menu = None;
|
||||
for command in [
|
||||
Command::Display,
|
||||
Command::Paths,
|
||||
Command::MakeExe,
|
||||
Command::MakeLibrary,
|
||||
Command::Find,
|
||||
Command::Replace,
|
||||
Command::About,
|
||||
] {
|
||||
app.execute(command);
|
||||
assert_theme(&mut app, "Dialog");
|
||||
if let Some(d) = &mut app.dialog {
|
||||
d.error = "Prüffehler: ungültiger Wert".into();
|
||||
}
|
||||
assert_theme(&mut app, "Dialogfehler");
|
||||
app.dialog = None;
|
||||
}
|
||||
for command in [
|
||||
Command::Project,
|
||||
Command::Debug,
|
||||
Command::Calls,
|
||||
Command::HelpContents,
|
||||
Command::Keyboard,
|
||||
Command::UsingHelp,
|
||||
] {
|
||||
app.execute(command);
|
||||
assert_theme(&mut app, "Projekt/Debugger/Hilfe");
|
||||
plain(&mut app, K::Tab);
|
||||
assert_theme(&mut app, "Fokus/Link");
|
||||
}
|
||||
let path = t.0.join("debug-colors.bas");
|
||||
fs::write(&path, "x%=1\nx%=2\nPRINT x%\nEND\n").unwrap();
|
||||
let mut debugger = t.app();
|
||||
debugger.load_initial_project(path).unwrap();
|
||||
plain(&mut debugger, K::Down);
|
||||
debugger.execute(Command::Breakpoint);
|
||||
assert_theme(&mut debugger, "Ungebundener Breakpoint");
|
||||
debugger.execute(Command::Start);
|
||||
for _ in 0..30 {
|
||||
debugger.tick_execution(0);
|
||||
if debugger.execution == tb_ide::app::Execution::Paused {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(debugger.execution, tb_ide::app::Execution::Paused);
|
||||
assert_theme(&mut debugger, "Gebundener Breakpoint und Ausführungszeile");
|
||||
let (_, marks) = draw(&mut debugger);
|
||||
assert!(marks
|
||||
.content
|
||||
.iter()
|
||||
.any(|c| c.symbol() == "▶" && c.bg == tb_ide::render::dos(14)));
|
||||
// Ein absichtlich schlechtes Standardpaar muss auch die Zellprüfung brechen.
|
||||
let mut bad = t.app();
|
||||
bad.options.colors[3] = (15, 7);
|
||||
assert!(
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| assert_theme(
|
||||
&mut bad,
|
||||
"Negativprobe"
|
||||
)))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_color_commands_use_explicit_values_and_never_modify_terminal_palette() {
|
||||
crossterm::style::force_color_output(true);
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_designer_chrome_and_handles_preserve_program_palette() {
|
||||
let t = Temp::new();
|
||||
let mut app = t.app();
|
||||
app.execute(Command::NewForm);
|
||||
let root = app.design_objects().unwrap()[0].0;
|
||||
app.design_select(root, false).unwrap();
|
||||
let (_, b) = draw(&mut app);
|
||||
let handles: Vec<_> = b.content.iter().filter(|c| c.symbol() == "■").collect();
|
||||
assert!(!handles.is_empty());
|
||||
for c in handles {
|
||||
assert_eq!(
|
||||
(c.fg, c.bg),
|
||||
(tb_ide::render::dos(0), tb_ide::render::dos(14))
|
||||
);
|
||||
assert!(theme_contrast(c.fg, c.bg) >= 4.5);
|
||||
}
|
||||
let form = app.design_form().unwrap().clone();
|
||||
let (_, screen) = tb_ide::designer::preview(&form, (100, 30)).unwrap();
|
||||
let mut buf = ratatui::buffer::Buffer::empty(ratatui::layout::Rect::new(0, 0, 100, 30));
|
||||
ratatui::widgets::Widget::render(tb_ui::screen::ScreenWidget(&screen), buf.area, &mut buf);
|
||||
for (i, c) in buf.content.iter().enumerate() {
|
||||
let original = screen.cell(i / 100 + 1, i % 100 + 1);
|
||||
assert_eq!(c.bg, tb_ui::screen::basic_color(original.bg));
|
||||
}
|
||||
// Vorschau minimieren: ab hier gehören alle sichtbaren Zellen zur IDE.
|
||||
app.execute(Command::Minimize);
|
||||
for command in [Command::Toolbox, Command::Palette, Command::MenuDesign] {
|
||||
app.execute(command);
|
||||
assert_theme(&mut app, "Designer-Werkzeug");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user