| 1 | //! Five atmospheres over real components, one entry per [`WaterPalette`]. |
| 2 | //! |
| 3 | //! Every entry paints the same illustrative workspace — a heading, the four |
| 4 | //! state words, a deep-water panel with the whale and a little marine life — |
| 5 | //! through the usual components and then finishes it with one |
| 6 | //! [`Ombre::apply`], so the five previews differ only in the atmosphere. All |
| 7 | //! content is illustrative host data, never a live measurement. |
| 8 | |
| 9 | use std::time::Duration; |
| 10 | |
| 11 | use ratatui::{buffer::Buffer, layout::Rect}; |
| 12 | |
| 13 | use super::Entry; |
| 14 | use crate::{ |
| 15 | BubbleField, Depth, FishSchool, HabitatDensity, Heading, Message, MotionMode, Ombre, Paint, |
| 16 | Panel, Picker, PickerItem, PickerState, Role, State, StatusMark, Theme, WaterPalette, Whale, |
| 17 | WhaleState, |
| 18 | }; |
| 19 | |
| 20 | /// The rows a band starts at, clamped to the area it is cut from. |
| 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 | /// One atmosphere over a workspace card, the four state words and the whale, |
| 32 | /// finished last by the ombre. |
| 33 | fn scene(area: Rect, buf: &mut Buffer, theme: &Theme, palette: WaterPalette) { |
| 34 | let area = area.intersection(buf.area); |
| 35 | if area.is_empty() { |
| 36 | return; |
| 37 | } |
| 38 | Heading::new(format!("Codewhale / {} atmosphere", palette.name())) |
| 39 | .meta("Finishing pass") |
| 40 | .paint(band(area, 0, 1), buf, theme); |
| 41 | let body = band(area, 1, area.height.saturating_sub(1)); |
| 42 | if body.is_empty() { |
| 43 | return; |
| 44 | } |
| 45 | |
| 46 | let rail_width = 20.min(body.width); |
| 47 | let rail = Rect { |
| 48 | width: rail_width, |
| 49 | ..body |
| 50 | }; |
| 51 | let inner = Panel::new(Depth::Deep) |
| 52 | .title("Palettes") |
| 53 | .aside("wash") |
| 54 | .draw(rail, buf, theme); |
| 55 | let items: Vec<PickerItem> = WaterPalette::ALL |
| 56 | .iter() |
| 57 | .map(|palette| PickerItem::new(palette.name())) |
| 58 | .collect(); |
| 59 | let selected = WaterPalette::ALL |
| 60 | .iter() |
| 61 | .position(|candidate| *candidate == palette) |
| 62 | .unwrap_or(0); |
| 63 | Picker::new(&items, PickerState::new(selected)).paint(inner, buf, theme); |
| 64 | |
| 65 | let gap = u16::from(body.width > rail_width); |
| 66 | let stage = Rect { |
| 67 | x: rail.right().saturating_add(gap), |
| 68 | width: body |
| 69 | .right() |
| 70 | .saturating_sub(rail.right().saturating_add(gap)), |
| 71 | ..body |
| 72 | }; |
| 73 | if stage.is_empty() { |
| 74 | return; |
| 75 | } |
| 76 | Message::new("You", "Finish this scene with one atmosphere.") |
| 77 | .role(Role::Primary) |
| 78 | .paint(band(stage, 0, 2), buf, theme); |
| 79 | |
| 80 | let turn = Panel::new(Depth::Raised) |
| 81 | .title("Turn") |
| 82 | .draw(band(stage, 2, 7), buf, theme); |
| 83 | for (row, state) in [State::Working, State::NeedsYou, State::Failed, State::Done] |
| 84 | .into_iter() |
| 85 | .enumerate() |
| 86 | { |
| 87 | let row = u16::try_from(row).unwrap_or(0); |
| 88 | StatusMark::new(state).paint(band(turn, row, 1), buf, theme); |
| 89 | } |
| 90 | |
| 91 | let water = Panel::new(Depth::Deep).title("Deep water").draw( |
| 92 | band(stage, 9, stage.height.saturating_sub(9)), |
| 93 | buf, |
| 94 | theme, |
| 95 | ); |
| 96 | let whale = Rect { |
| 97 | width: 24.min(water.width), |
| 98 | height: water.height.min(11), |
| 99 | ..water |
| 100 | }; |
| 101 | Whale::new(WhaleState::Busy).paint(whale, buf, theme); |
| 102 | let life = Rect { |
| 103 | x: water.x.saturating_add(whale.width.saturating_add(1)), |
| 104 | width: water.width.saturating_sub(whale.width.saturating_add(1)), |
| 105 | height: water.height.min(7), |
| 106 | ..water |
| 107 | }; |
| 108 | if !life.is_empty() { |
| 109 | FishSchool::new(Duration::from_millis(2_400), MotionMode::Full) |
| 110 | .density(HabitatDensity::Normal) |
| 111 | .paint(life, buf, theme); |
| 112 | BubbleField::new(Duration::from_millis(1_200), MotionMode::Full) |
| 113 | .density(HabitatDensity::Normal) |
| 114 | .paint(life, buf, theme); |
| 115 | } |
| 116 | |
| 117 | // The finishing pass, last: one cohesive atmosphere across the scene. |
| 118 | Ombre::new(palette).apply(area, buf, theme); |
| 119 | } |
| 120 | |
| 121 | fn ocean(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 122 | scene(area, buf, theme, WaterPalette::Ocean); |
| 123 | } |
| 124 | |
| 125 | fn lagoon(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 126 | scene(area, buf, theme, WaterPalette::Lagoon); |
| 127 | } |
| 128 | |
| 129 | fn dusk(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 130 | scene(area, buf, theme, WaterPalette::Dusk); |
| 131 | } |
| 132 | |
| 133 | fn coral(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 134 | scene(area, buf, theme, WaterPalette::Coral); |
| 135 | } |
| 136 | |
| 137 | fn graphite(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 138 | scene(area, buf, theme, WaterPalette::Graphite); |
| 139 | } |
| 140 | |
| 141 | pub(crate) fn entries() -> Vec<Entry> { |
| 142 | vec![ |
| 143 | Entry { |
| 144 | name: "atmosphere-ocean", |
| 145 | width: 104, |
| 146 | height: 24, |
| 147 | draw: ocean, |
| 148 | }, |
| 149 | Entry { |
| 150 | name: "atmosphere-lagoon", |
| 151 | width: 104, |
| 152 | height: 24, |
| 153 | draw: lagoon, |
| 154 | }, |
| 155 | Entry { |
| 156 | name: "atmosphere-dusk", |
| 157 | width: 104, |
| 158 | height: 24, |
| 159 | draw: dusk, |
| 160 | }, |
| 161 | Entry { |
| 162 | name: "atmosphere-coral", |
| 163 | width: 104, |
| 164 | height: 24, |
| 165 | draw: coral, |
| 166 | }, |
| 167 | Entry { |
| 168 | name: "atmosphere-graphite", |
| 169 | width: 104, |
| 170 | height: 24, |
| 171 | draw: graphite, |
| 172 | }, |
| 173 | ] |
| 174 | } |
| 175 |