| 1 | //! Fixed caller phases over the actual Codewhale terminal water column. |
| 2 | //! Context values and completion clocks are illustrative inputs, not receipts. |
| 3 | |
| 4 | use std::time::Duration; |
| 5 | |
| 6 | use ratatui::{ |
| 7 | buffer::Buffer, |
| 8 | layout::Rect, |
| 9 | style::Modifier, |
| 10 | text::{Line, Span}, |
| 11 | widgets::{Paragraph, Widget}, |
| 12 | }; |
| 13 | use unicode_width::UnicodeWidthStr; |
| 14 | |
| 15 | use super::Entry; |
| 16 | use crate::{ |
| 17 | Depth, Heading, Message, MotionMode, Paint, Panel, Role, State, StatusMark, Theme, |
| 18 | ocean::{OceanCausticFacts, OceanColumn, OceanPaintFacts, OceanPhase, ocean_semantic_surfaces}, |
| 19 | }; |
| 20 | |
| 21 | fn band(area: Rect, top: u16, height: u16) -> Rect { |
| 22 | let top = top.min(area.height); |
| 23 | Rect::new( |
| 24 | area.x, |
| 25 | area.y.saturating_add(top), |
| 26 | area.width, |
| 27 | height.min(area.height.saturating_sub(top)), |
| 28 | ) |
| 29 | } |
| 30 | |
| 31 | fn column(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 32 | let area = area.intersection(buf.area); |
| 33 | Heading::new("Codewhale / native ocean") |
| 34 | .meta("Working / fixture") |
| 35 | .paint(band(area, 0, 1), buf, theme); |
| 36 | Message::new("You", "Keep the code and the decision clear.") |
| 37 | .role(Role::Primary) |
| 38 | .paint(band(area, 2, 3), buf, theme); |
| 39 | Message::new( |
| 40 | "Codewhale", |
| 41 | "One continuous column behind the conversation.", |
| 42 | ) |
| 43 | .paint(band(area, 6, 3), buf, theme); |
| 44 | |
| 45 | let selected = band(area, 10, 1); |
| 46 | buf.set_style(selected, theme.bg(Role::Selected)); |
| 47 | Heading::new("Selected / src/ocean.rs") |
| 48 | .sub() |
| 49 | .paint(selected, buf, theme); |
| 50 | let surface = Panel::new(Depth::Raised) |
| 51 | .title("Semantic surface / own ground") |
| 52 | .draw(band(area, 12, 4), buf, theme); |
| 53 | StatusMark::new(State::Working).paint(surface, buf, theme); |
| 54 | |
| 55 | OceanColumn::new(Duration::from_millis(22_500), MotionMode::Full) |
| 56 | .phase(OceanPhase::Working) |
| 57 | .apply(area, buf, theme); |
| 58 | } |
| 59 | |
| 60 | fn phases_with(area: Rect, buf: &mut Buffer, theme: &Theme, motion: MotionMode) { |
| 61 | let area = area.intersection(buf.area); |
| 62 | let phases = [ |
| 63 | (OceanPhase::Working, State::Working, "Working / quiet light"), |
| 64 | ( |
| 65 | OceanPhase::Verifying, |
| 66 | State::Working, |
| 67 | "Verifying / quiet light", |
| 68 | ), |
| 69 | ( |
| 70 | OceanPhase::Approval, |
| 71 | State::NeedsYou, |
| 72 | "Approval / steady warm water", |
| 73 | ), |
| 74 | (OceanPhase::Failed, State::Failed, "Failed / steady tint"), |
| 75 | ( |
| 76 | OceanPhase::Done, |
| 77 | State::Done, |
| 78 | if motion.animates() { |
| 79 | "Done / completion at 320 ms" |
| 80 | } else { |
| 81 | "Done / static completion" |
| 82 | }, |
| 83 | ), |
| 84 | ]; |
| 85 | for (index, (phase, state, label)) in phases.into_iter().enumerate() { |
| 86 | let strip = band(area, index as u16 * 4, 4); |
| 87 | Heading::new(label).paint(band(strip, 0, 1), buf, theme); |
| 88 | StatusMark::new(state).paint(band(strip, 1, 1), buf, theme); |
| 89 | OceanColumn::new(Duration::from_millis(22_500), motion) |
| 90 | .phase(phase) |
| 91 | .completion_elapsed(Duration::from_millis(320)) |
| 92 | .viewport(area) |
| 93 | .apply(strip, buf, theme); |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | fn phases(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 98 | phases_with(area, buf, theme, MotionMode::Full); |
| 99 | } |
| 100 | |
| 101 | fn reduced(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 102 | phases_with(area, buf, theme, MotionMode::Reduced); |
| 103 | } |
| 104 | |
| 105 | fn context(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 106 | let area = area.intersection(buf.area); |
| 107 | for (index, percent) in [0, 50, 100].into_iter().enumerate() { |
| 108 | let left = (u32::from(area.width) * index as u32 / 3) as u16; |
| 109 | let right = (u32::from(area.width) * (index as u32 + 1) / 3) as u16; |
| 110 | let column = Rect { |
| 111 | x: area.x.saturating_add(left), |
| 112 | width: right.saturating_sub(left), |
| 113 | ..area |
| 114 | }; |
| 115 | Heading::new(format!("{percent}% context")).paint(band(column, 0, 1), buf, theme); |
| 116 | Message::new("Fixture", "Fullness raises the abyss.").paint( |
| 117 | band(column, 3, column.height.saturating_sub(3)), |
| 118 | buf, |
| 119 | theme, |
| 120 | ); |
| 121 | OceanColumn::new(Duration::ZERO, MotionMode::Still) |
| 122 | .context_percent(percent) |
| 123 | .apply(column, buf, theme); |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | // This native path supplies presentation facts from a real rendered pane. |
| 128 | // It keeps raw ink/style facts and semantic padding while sharing cached rows |
| 129 | // between the base water and caustic passes. No terminal evidence is invented. |
| 130 | fn native_guarded(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 131 | let area = area.intersection(buf.area); |
| 132 | let base = theme.bg(Role::Background); |
| 133 | let mut rows = vec![Line::from(""); 4]; |
| 134 | rows.push(Line::from(vec![Span::styled( |
| 135 | "Native / guarded ocean", |
| 136 | theme.fg(Role::Foreground), |
| 137 | )])); |
| 138 | rows.push(Line::from(vec![Span::styled( |
| 139 | "[Selected] source keeps its own ground", |
| 140 | theme.bg(Role::Selected).patch(theme.fg(Role::Foreground)), |
| 141 | )])); |
| 142 | rows.push(Line::styled("", base)); |
| 143 | rows.push(Line::from(vec![Span::styled( |
| 144 | "Explicit blank padding is semantic", |
| 145 | base.patch(theme.fg(Role::Foreground)), |
| 146 | )])); |
| 147 | rows.push(Line::from(vec![Span::styled( |
| 148 | "Reversed surface", |
| 149 | theme.fg(Role::Foreground).add_modifier(Modifier::REVERSED), |
| 150 | )])); |
| 151 | rows.push(Line::styled( |
| 152 | "Caller phase: Working / fixture", |
| 153 | theme.fg(Role::Dim), |
| 154 | )); |
| 155 | Paragraph::new(rows.clone()).render(area, buf); |
| 156 | let column = OceanColumn::new(Duration::from_millis(22_500), MotionMode::Full) |
| 157 | .phase(OceanPhase::Working) |
| 158 | .viewport(area); |
| 159 | let samples: Vec<_> = (area.y..area.bottom()) |
| 160 | .map_while(|y| column.color_at_y(y, area, theme)) |
| 161 | .collect(); |
| 162 | let protected = ocean_semantic_surfaces(&rows, area, str::width); |
| 163 | let paint = OceanPaintFacts { |
| 164 | ground: base.bg.unwrap_or(ratatui::style::Color::Reset), |
| 165 | sample_top: area.y, |
| 166 | samples: &samples, |
| 167 | protected: &protected, |
| 168 | }; |
| 169 | column.apply_native(area, buf, theme, &paint, |cell, _| cell.fg); |
| 170 | column.apply_caustics( |
| 171 | area, |
| 172 | buf, |
| 173 | theme, |
| 174 | &OceanCausticFacts { |
| 175 | paint, |
| 176 | elapsed: Duration::from_millis(480), |
| 177 | band_rows: 4, |
| 178 | }, |
| 179 | ); |
| 180 | } |
| 181 | |
| 182 | pub(crate) fn entries() -> Vec<Entry> { |
| 183 | vec![ |
| 184 | Entry { |
| 185 | name: "ocean-column", |
| 186 | width: 80, |
| 187 | height: 16, |
| 188 | draw: column, |
| 189 | }, |
| 190 | Entry { |
| 191 | name: "ocean-phases", |
| 192 | width: 80, |
| 193 | height: 20, |
| 194 | draw: phases, |
| 195 | }, |
| 196 | Entry { |
| 197 | name: "ocean-context", |
| 198 | width: 80, |
| 199 | height: 12, |
| 200 | draw: context, |
| 201 | }, |
| 202 | Entry { |
| 203 | name: "ocean-reduced", |
| 204 | width: 80, |
| 205 | height: 20, |
| 206 | draw: reduced, |
| 207 | }, |
| 208 | Entry { |
| 209 | name: "ocean-native-guarded", |
| 210 | width: 80, |
| 211 | height: 16, |
| 212 | draw: native_guarded, |
| 213 | }, |
| 214 | ] |
| 215 | } |
| 216 |