| 1 | use super::*; |
| 2 | use crate::tui::color_compat::ColorCompatBackend; |
| 3 | use codewhale_ratatui::ocean::{OceanPaintFacts, ocean_semantic_surfaces}; |
| 4 | use ratatui::{ |
| 5 | style::{Modifier, Style}, |
| 6 | text::{Line, Span}, |
| 7 | }; |
| 8 | |
| 9 | fn caps(depth: codewhale_palette::ColorDepth) -> codewhale_ratatui::Caps { |
| 10 | ColorCompatBackend::new(std::io::sink(), depth, codewhale_palette::PaletteMode::Dark) |
| 11 | .native_ocean_caps() |
| 12 | } |
| 13 | fn column(area: Rect, phase: ShellPhase, elapsed: u128) -> OceanColumn { |
| 14 | OceanColumn::new( |
| 15 | OceanRamp::for_theme(&codewhale_palette::UNDERWATER_UI_THEME).unwrap(), |
| 16 | area, |
| 17 | elapsed, |
| 18 | None, |
| 19 | phase, |
| 20 | true, |
| 21 | 750, |
| 22 | 30, |
| 23 | ) |
| 24 | .with_paint_caps(Some(caps(codewhale_palette::ColorDepth::TrueColor))) |
| 25 | } |
| 26 | fn ordinary(area: Rect) -> Buffer { |
| 27 | let mut buf = Buffer::empty(area); |
| 28 | for cell in &mut buf.content { |
| 29 | cell.set_bg(codewhale_palette::UNDERWATER_UI_THEME.surface_bg) |
| 30 | .set_fg(Color::Rgb(255, 255, 255)); |
| 31 | } |
| 32 | buf |
| 33 | } |
| 34 | #[test] |
| 35 | fn guarded_ocean_backend_depth_is_forwarded_and_explicit_pane_supplies_ground_evidence() { |
| 36 | use codewhale_ratatui::{color::ColorDepth as Depth, detect::Appearance}; |
| 37 | for (host, native) in [ |
| 38 | (codewhale_palette::ColorDepth::TrueColor, Depth::TrueColor), |
| 39 | (codewhale_palette::ColorDepth::Ansi256, Depth::Ansi256), |
| 40 | (codewhale_palette::ColorDepth::Ansi16, Depth::Ansi16), |
| 41 | (codewhale_palette::ColorDepth::Monochrome, Depth::Monochrome), |
| 42 | ] { |
| 43 | let reported = caps(host); |
| 44 | assert_eq!(reported.depth, native); |
| 45 | assert_eq!( |
| 46 | reported.appearance, |
| 47 | Appearance::Unknown, |
| 48 | "OS-dark mode is not measured ground" |
| 49 | ); |
| 50 | let area = Rect::new(7, 5, 12, 4); |
| 51 | let mut buf = ordinary(area); |
| 52 | let before = buf.clone(); |
| 53 | let column = column(area, ShellPhase::Idle, 0).with_paint_caps(Some(reported)); |
| 54 | column.paint_matching_native( |
| 55 | area, |
| 56 | &mut buf, |
| 57 | codewhale_palette::UNDERWATER_UI_THEME.surface_bg, |
| 58 | &codewhale_palette::UNDERWATER_UI_THEME, |
| 59 | &[], |
| 60 | ); |
| 61 | if native == Depth::TrueColor { |
| 62 | assert_ne!(buf, before, "actual dark pane supplies its own evidence"); |
| 63 | } else { |
| 64 | assert_eq!( |
| 65 | buf, before, |
| 66 | "fallback retains the selected flat pane ground" |
| 67 | ); |
| 68 | } |
| 69 | } |
| 70 | let area = Rect::new(7, 5, 12, 4); |
| 71 | let col = column(area, ShellPhase::Idle, 0); |
| 72 | for ground in [Color::Reset, Color::White, Color::Rgb(250, 250, 250)] { |
| 73 | let mut buf = ordinary(area); |
| 74 | buf.set_style(area, Style::default().bg(ground)); |
| 75 | let before = buf.clone(); |
| 76 | col.paint_matching_native( |
| 77 | area, |
| 78 | &mut buf, |
| 79 | ground, |
| 80 | &codewhale_palette::UNDERWATER_UI_THEME, |
| 81 | &[], |
| 82 | ); |
| 83 | assert_eq!( |
| 84 | buf, before, |
| 85 | "unknown or known-light explicit grounds cannot earn a dark field" |
| 86 | ); |
| 87 | } |
| 88 | } |
| 89 | #[test] |
| 90 | fn guarded_ocean_whole_buffer_matches_frozen_safe_painter_across_phase_size_and_cache_rows() { |
| 91 | let theme = codewhale_palette::UNDERWATER_UI_THEME; |
| 92 | for phase in [ |
| 93 | ShellPhase::Idle, |
| 94 | ShellPhase::Typing, |
| 95 | ShellPhase::Working, |
| 96 | ShellPhase::Verifying, |
| 97 | ShellPhase::Waiting, |
| 98 | ShellPhase::Approval, |
| 99 | ShellPhase::Done, |
| 100 | ShellPhase::Failed, |
| 101 | ] { |
| 102 | for (width, height) in [(4, 3), (40, 10), (80, 24)] { |
| 103 | for elapsed in [0, 320, 22500, 89999] { |
| 104 | let area = Rect::new(7, 5, width, height); |
| 105 | let col = column(area, phase, elapsed); |
| 106 | let mut actual = ordinary(area); |
| 107 | for x in area.x..area.right() { |
| 108 | actual[(x, area.y)].set_symbol("x"); |
| 109 | } |
| 110 | let mut expected = actual.clone(); |
| 111 | guarded_legacy::LegacyColumn(col).paint_matching( |
| 112 | area, |
| 113 | &mut expected, |
| 114 | theme.surface_bg, |
| 115 | ); |
| 116 | col.paint_matching_native(area, &mut actual, theme.surface_bg, &theme, &[]); |
| 117 | assert_eq!( |
| 118 | actual, expected, |
| 119 | "phase={phase:?} area={area:?} elapsed={elapsed}" |
| 120 | ); |
| 121 | // Actual transcript path preserves the same frame ramp cache receipts. |
| 122 | let ramp = crate::tui::ambient_life::frame_ocean_ramp( |
| 123 | &col, |
| 124 | area.height, |
| 125 | area.y, |
| 126 | elapsed, |
| 127 | col.phase_tag(), |
| 128 | col.ramp_fingerprint(), |
| 129 | ); |
| 130 | let facts = OceanPaintFacts { |
| 131 | ground: theme.surface_bg, |
| 132 | sample_top: area.y, |
| 133 | samples: &ramp, |
| 134 | protected: &[], |
| 135 | }; |
| 136 | let mut cached = ordinary(area); |
| 137 | let mut old = cached.clone(); |
| 138 | guarded_legacy::LegacyWater { |
| 139 | ocean_column: Some(col), |
| 140 | lines: vec![], |
| 141 | background: theme.surface_bg, |
| 142 | ocean_elapsed_ms: elapsed, |
| 143 | } |
| 144 | .paint(area, &mut old); |
| 145 | col.paint_native(area, &mut cached, &theme, &facts); |
| 146 | assert_eq!(cached, old); |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | } |
| 151 | #[test] |
| 152 | fn guarded_ocean_semantic_alias_reverse_unknown_ink_and_style_facts_survive_final_shell_pass() { |
| 153 | let theme = codewhale_palette::UNDERWATER_UI_THEME; |
| 154 | let area = Rect::new(7, 5, 20, 4); |
| 155 | let col = column(area, ShellPhase::Idle, 0); |
| 156 | let rows = vec![Line::from(vec![ |
| 157 | Span::raw("plain "), |
| 158 | Span::styled( |
| 159 | "selected", |
| 160 | Style::default() |
| 161 | .bg(theme.surface_bg) |
| 162 | .add_modifier(Modifier::ITALIC), |
| 163 | ), |
| 164 | ])]; |
| 165 | let protected = |
| 166 | ocean_semantic_surfaces(&rows, area, crate::tui::ui_text::grapheme_display_width); |
| 167 | assert_eq!(protected, vec![Rect::new(13, 5, 8, 1)]); |
| 168 | let mut buf = ordinary(area); |
| 169 | buf[(7, 6)].set_symbol("x").set_fg(Color::Reset); |
| 170 | buf[(8, 6)].set_symbol("x").set_fg(Color::Magenta); |
| 171 | buf[(9, 6)].set_symbol("x").set_fg(col.color_at_y(6)); |
| 172 | buf[(10, 6)].set_bg(theme.selection_bg); |
| 173 | buf[(11, 6)] |
| 174 | .modifier |
| 175 | .insert(Modifier::REVERSED | Modifier::BOLD); |
| 176 | let before = buf.clone(); |
| 177 | col.paint_matching_native(area, &mut buf, theme.surface_bg, &theme, &protected); |
| 178 | for x in [7, 8, 10, 11] { |
| 179 | assert_eq!(buf[(x, 6)], before[(x, 6)]); |
| 180 | } |
| 181 | // The terminal backend lifts water-coloured glyphs to visible body ink; |
| 182 | // the guard projects that same adaptation before admitting the backdrop. |
| 183 | assert_eq!(buf[(9, 6)].fg, before[(9, 6)].fg); |
| 184 | assert_eq!(buf[(9, 6)].modifier, before[(9, 6)].modifier); |
| 185 | assert_eq!(buf[(9, 6)].bg, col.color_at_y(6)); |
| 186 | for x in 13..21 { |
| 187 | assert_eq!( |
| 188 | buf[(x, 5)], |
| 189 | before[(x, 5)], |
| 190 | "explicit style aliases remain semantic" |
| 191 | ); |
| 192 | } |
| 193 | assert_eq!(buf[(7, 5)].bg, col.color_at_y(5)); |
| 194 | } |
| 195 | #[test] |
| 196 | fn guarded_ocean_uses_backend_projected_custom_ink_and_live_supporting_floor() { |
| 197 | let mut theme = codewhale_palette::UNDERWATER_UI_THEME; |
| 198 | theme.text_body = Color::Rgb(255, 255, 255); |
| 199 | theme.border = Color::Rgb(111, 149, 181); |
| 200 | let area = Rect::new(7, 5, 8, 4); |
| 201 | let col = column(area, ShellPhase::Idle, 0); |
| 202 | let mut buf = ordinary(area); |
| 203 | buf[(7, 5)] |
| 204 | .set_symbol("x") |
| 205 | .set_fg(codewhale_palette::TEXT_BODY); |
| 206 | buf[(8, 5)] |
| 207 | .set_symbol("|") |
| 208 | .set_fg(theme.border) |
| 209 | .modifier |
| 210 | .insert(Modifier::ITALIC); |
| 211 | let before = buf.clone(); |
| 212 | let projected = crate::tui::color_compat::project_ocean_ink( |
| 213 | &buf[(7, 5)], |
| 214 | col.color_at_y(5), |
| 215 | caps(codewhale_palette::ColorDepth::TrueColor).depth, |
| 216 | &theme, |
| 217 | ); |
| 218 | // Underwater retains its body token; custom supporting ink still follows |
| 219 | // the live theme and the backend's contrast projection. |
| 220 | assert_eq!(projected, codewhale_palette::TEXT_BODY); |
| 221 | col.paint_matching_native(area, &mut buf, theme.surface_bg, &theme, &[]); |
| 222 | assert_eq!(buf[(7, 5)].bg, col.color_at_y(5)); |
| 223 | assert_eq!( |
| 224 | buf[(7, 5)].fg, |
| 225 | before[(7, 5)].fg, |
| 226 | "guard never rewrites source ink" |
| 227 | ); |
| 228 | assert_eq!(buf[(8, 5)].modifier, before[(8, 5)].modifier); |
| 229 | assert_eq!(buf[(8, 5)].bg, col.color_at_y(5)); |
| 230 | } |
| 231 | #[test] |
| 232 | fn guarded_ocean_caustic_safe_pixels_match_frozen_math_and_spare_semantic_blank_cells() { |
| 233 | let area = Rect::new(7, 5, 80, 24); |
| 234 | let col = column(area, ShellPhase::Working, 480); |
| 235 | let mut actual = ordinary(area); |
| 236 | col.paint_matching_native( |
| 237 | area, |
| 238 | &mut actual, |
| 239 | codewhale_palette::UNDERWATER_UI_THEME.surface_bg, |
| 240 | &codewhale_palette::UNDERWATER_UI_THEME, |
| 241 | &[], |
| 242 | ); |
| 243 | let mut expected = actual.clone(); |
| 244 | guarded_legacy::apply_caustic_shimmer(area, &mut expected, &col, 480, true, &[]); |
| 245 | crate::tui::ambient_life::apply_caustic_shimmer(area, &mut actual, &col, 480, true, &[]); |
| 246 | assert_eq!(actual, expected); |
| 247 | let rows = [Line::styled( |
| 248 | "", |
| 249 | Style::default().bg(col.color_at_y(area.y)), |
| 250 | )]; |
| 251 | let mut buf = ordinary(area); |
| 252 | col.paint_matching_native( |
| 253 | area, |
| 254 | &mut buf, |
| 255 | codewhale_palette::UNDERWATER_UI_THEME.surface_bg, |
| 256 | &codewhale_palette::UNDERWATER_UI_THEME, |
| 257 | &[], |
| 258 | ); |
| 259 | buf[(10, 6)].set_bg(Color::Rgb(73, 89, 107)); |
| 260 | buf[(13, 6)].modifier.insert(Modifier::REVERSED); |
| 261 | let before = buf.clone(); |
| 262 | crate::tui::ambient_life::apply_caustic_shimmer(area, &mut buf, &col, 480, true, &rows); |
| 263 | for x in area.x..area.right() { |
| 264 | assert_eq!(buf[(x, area.y)], before[(x, area.y)]); |
| 265 | } |
| 266 | assert_eq!(buf[(10, 6)], before[(10, 6)]); |
| 267 | assert_eq!(buf[(13, 6)], before[(13, 6)]); |
| 268 | } |
| 269 | #[test] |
| 270 | fn guarded_ocean_missing_capability_still_motion_and_clip_cannot_modify_guards() { |
| 271 | let area = Rect::new(7, 5, 80, 24); |
| 272 | let col = column(area, ShellPhase::Working, 480); |
| 273 | for col in [ |
| 274 | col.with_paint_caps(None), |
| 275 | OceanColumn::new( |
| 276 | OceanRamp::for_theme(&codewhale_palette::UNDERWATER_UI_THEME).unwrap(), |
| 277 | area, |
| 278 | 480, |
| 279 | None, |
| 280 | ShellPhase::Working, |
| 281 | false, |
| 282 | 0, |
| 283 | 0, |
| 284 | ) |
| 285 | .with_paint_caps(Some(caps(codewhale_palette::ColorDepth::TrueColor))), |
| 286 | ] { |
| 287 | let mut buf = ordinary(area); |
| 288 | // Establish real ordinary water first, so missing caps and Still |
| 289 | // must stop a pass that would otherwise have eligible pixels. |
| 290 | column(area, ShellPhase::Working, 480).paint_matching_native( |
| 291 | area, |
| 292 | &mut buf, |
| 293 | codewhale_palette::UNDERWATER_UI_THEME.surface_bg, |
| 294 | &codewhale_palette::UNDERWATER_UI_THEME, |
| 295 | &[], |
| 296 | ); |
| 297 | let before = buf.clone(); |
| 298 | crate::tui::ambient_life::apply_caustic_shimmer(area, &mut buf, &col, 480, true, &[]); |
| 299 | assert_eq!(buf, before); |
| 300 | } |
| 301 | let mut buf = ordinary(area); |
| 302 | let before = buf.clone(); |
| 303 | let requested = Rect::new(9, 7, 8, 3); |
| 304 | col.paint_matching_native( |
| 305 | requested, |
| 306 | &mut buf, |
| 307 | codewhale_palette::UNDERWATER_UI_THEME.surface_bg, |
| 308 | &codewhale_palette::UNDERWATER_UI_THEME, |
| 309 | &[], |
| 310 | ); |
| 311 | for y in area.y..area.bottom() { |
| 312 | for x in area.x..area.right() { |
| 313 | if !requested.contains((x, y).into()) { |
| 314 | assert_eq!(buf[(x, y)], before[(x, y)]); |
| 315 | } |
| 316 | } |
| 317 | } |
| 318 | } |
| 319 |