1use qframe::geometry::{Rect, Size};
7use qframe::icons::GlyphMode;
8use qframe::style::CellStyle;
9use qframe::text;
10use qframe::widget::{MeasureCx, PaintCx, Widget};
11
12const ART: [&str; ROWS as usize] = [
15 " ▄████████",
16 " ▄███▀▀▀▀███",
17 "███▀ ███ ██████ ██████ █████▄ ██████",
18 "███ ███ ██ ██ ██ ██ ██ ██▄▄▄▄",
19 "███▄▄▄▄▄ ▀█ ██ ██ ██ ██ ██ ██▀▀▀▀",
20 "█████████▄ ██████ ██████ █████▀ ██████",
21];
22
23const ROWS: u16 = 6;
25
26const PLAIN: &str = "QCode";
29
30const ROWS_BELOW: u16 = 7;
34
35const COLOR: &str = "accent";
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
50pub struct Logo;
51
52impl Logo {
53 pub const WIDTH: u16 = 44;
55
56 #[must_use]
58 pub fn new() -> Self {
59 Self
60 }
61}
62
63impl<Msg: 'static> Widget<Msg> for Logo {
64 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
65 let width = available.width.min(Self::WIDTH);
66 let spare = available.height.saturating_sub(ROWS_BELOW);
68 if width >= Self::WIDTH && spare >= ROWS {
69 Size::new(width, ROWS)
70 } else if width >= text::width(PLAIN) && spare >= 1 {
71 Size::new(width, 1)
72 } else {
73 Size::new(0, 0)
74 }
75 }
76
77 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
78 if area.is_empty() {
79 return;
80 }
81 let color = cx.color(COLOR);
82 if area.width < Self::WIDTH || area.height < ROWS {
83 let name = area.centered(Size::new(text::width(PLAIN), 1));
84 cx.text(name.x, name.y, PLAIN, CellStyle::fg(color).with_bold(true), area.width);
85 return;
86 }
87 let drawing = area.centered(Size::new(Self::WIDTH, ROWS));
88 let painted = cx.env().glyph_mode() == GlyphMode::Ascii;
89 for (row, line) in ART.iter().enumerate() {
90 let y = drawing.y + i32::try_from(row).unwrap_or(0);
91 for (column, glyph) in line.chars().enumerate() {
92 if glyph == ' ' {
93 continue;
94 }
95 let x = drawing.x + i32::try_from(column).unwrap_or(0);
96 if painted {
97 cx.clear(Rect::new(x, y, 1, 1), color);
98 } else {
99 let mut buffer = [0u8; 4];
100 cx.text(x, y, glyph.encode_utf8(&mut buffer), CellStyle::fg(color), 1);
101 }
102 }
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use qframe::prelude::*;
110 use qframe::runtime::Harness;
111
112 use super::*;
113
114 struct Demo;
117
118 impl App for Demo {
119 type Msg = ();
120
121 fn update(&mut self, (): ()) -> Command<()> {
122 Command::none()
123 }
124
125 fn view(&self, ui: &mut View<'_, ()>) {
126 ui.add(Logo::new()).width(Length::Cells(Logo::WIDTH));
127 }
128 }
129
130 fn logo(width: u16, height: u16) -> Harness<Demo> {
131 let mut harness = Harness::new(Demo, width, height);
132 harness.set_glyph_mode(GlyphMode::Unicode).render();
133 harness
134 }
135
136 const TALL: u16 = ROWS + ROWS_BELOW;
138
139 #[test]
140 fn every_glyph_takes_exactly_one_cell() {
141 for line in ART {
142 for glyph in line.chars() {
143 let mut buffer = [0u8; 4];
144 let glyph = glyph.encode_utf8(&mut buffer);
145 assert_eq!(text::width(glyph), 1, "`{glyph}` is not one cell wide");
146 }
147 }
148 }
149
150 #[test]
151 fn the_rows_keep_the_widths_they_were_drawn_with() {
152 let widths: Vec<u16> = ART.iter().map(|line| text::width(line)).collect();
153 assert_eq!(widths, vec![12, 12, Logo::WIDTH, Logo::WIDTH, Logo::WIDTH, Logo::WIDTH]);
154 }
155
156 #[test]
157 fn unicode_draws_the_wordmark_in_the_accent_colour() {
158 let harness = logo(Logo::WIDTH, TALL);
159 let screen = harness.screen();
160 let drawn: Vec<&str> = screen.lines().take(usize::from(ROWS)).map(str::trim_end).collect();
161 assert_eq!(drawn, ART, "{screen}");
162 assert_eq!(harness.fg(3, 0), harness.env().theme().color(COLOR));
163 }
164
165 #[test]
166 fn nerd_font_draws_the_same_wordmark() {
167 let mut harness = logo(Logo::WIDTH, TALL);
168 let unicode = harness.screen();
169 harness.set_glyph_mode(GlyphMode::Nerd).render();
170 assert_eq!(harness.screen(), unicode);
171 }
172
173 #[test]
174 fn ascii_paints_the_shape_in_cells_of_colour() {
175 let mut harness = logo(Logo::WIDTH, TALL);
176 harness.set_glyph_mode(GlyphMode::Ascii).render();
177 let screen = harness.screen();
178 assert!(screen.is_ascii(), "{screen}");
179 assert!(screen.trim().is_empty(), "the shape is colour, not characters:\n{screen}");
180 let accent = harness.env().theme().color(COLOR);
181 assert_eq!(harness.bg(3, 0), accent, "the first lit cell of the top row");
182 assert_ne!(harness.bg(0, 0), accent, "the indent of the top row stays empty");
183 assert_eq!(harness.bg(10, 4), accent, "a half block rounds up to a whole cell");
185 assert_ne!(harness.bg(8, 4), accent, "the gap beside it stays empty");
186 }
187
188 #[test]
189 fn a_narrow_screen_writes_the_name_plainly_and_centred() {
190 let harness = logo(Logo::WIDTH - 1, TALL);
191 assert_eq!(harness.screen().lines().next(), Some(" QCode"));
192 }
193
194 #[test]
195 fn a_short_screen_writes_the_name_plainly() {
196 let harness = logo(Logo::WIDTH, TALL - 1);
197 assert_eq!(harness.screen().lines().next().map(str::trim), Some(PLAIN));
198 }
199
200 #[test]
201 fn a_screen_with_no_room_to_spare_drops_the_logo() {
202 let harness = logo(Logo::WIDTH, ROWS_BELOW);
203 assert!(harness.screen().trim().is_empty(), "{}", harness.screen());
204 }
205
206 #[test]
207 fn a_screen_too_narrow_even_for_the_name_drops_the_logo() {
208 let harness = logo(text::width(PLAIN) - 1, TALL);
209 assert!(harness.screen().trim().is_empty(), "{}", harness.screen());
210 }
211
212 #[test]
213 fn ascii_never_leaves_a_block_element_behind() {
214 let mut harness = logo(Logo::WIDTH - 1, TALL);
215 harness.set_glyph_mode(GlyphMode::Ascii).render();
216 assert!(harness.screen().is_ascii(), "{}", harness.screen());
217 }
218}