Skip to main content

banished_ui/
draw.rs

1//! Drawing: the tree as a list of sprite quads, clipped and coloured.
2
3use super::*;
4
5pub(super) fn argb_to_rgba(c: u32, alpha: u8) -> [u8; 4] {
6    let a = ((c >> 24) & 0xFF) * alpha as u32 / 255;
7    [(c >> 16) as u8, (c >> 8) as u8, c as u8, a as u8]
8}
9
10pub(super) fn modulate(a: u32, b: u32) -> u32 {
11    let ch = |s: u32| ((a >> s & 0xFF) * (b >> s & 0xFF) / 255) << s;
12    ch(24) | ch(16) | ch(8) | ch(0)
13}
14
15impl Ui {
16    /// The quads that draw `root` and its children, in drawing order, then the open combo lists
17    /// and the tooltip.
18    pub fn draw(&self, root: NodeId) -> Vec<Quad> {
19        let mut out = Vec::new();
20        self.draw_node(root, 0xFFFF_FFFF, None, &mut out);
21        self.draw_popups(root, &mut out);
22        self.draw_tooltip(root, &mut out);
23        out
24    }
25
26    /// `SpriteContext::AddLine`: a one-pixel line from `(x0, y0)` towards `(x1, y1)`, its
27    /// last pixel left off as a Direct3D line list draws it.
28    pub(crate) fn line_quads(
29        out: &mut Vec<Quad>,
30        s: &SpriteRef,
31        [x0, y0, x1, y1]: [i32; 4],
32        color: u32,
33        clip: Option<[i32; 4]>,
34    ) {
35        if y0 == y1 || x0 == x1 {
36            let rect = [x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)];
37            let rect = if y0 == y1 {
38                [rect[0], rect[1], rect[2], rect[1] + 1]
39            } else {
40                [rect[0], rect[1], rect[0] + 1, rect[3]]
41            };
42            Self::sprite_quad(out, s, rect, color, clip);
43            return;
44        }
45        let steps = (x1 - x0).abs().max((y1 - y0).abs());
46        for k in 0..steps {
47            let x = x0 + (x1 - x0) * k / steps;
48            let y = y0 + (y1 - y0) * k / steps;
49            Self::sprite_quad(out, s, [x, y, x + 1, y + 1], color, clip);
50        }
51    }
52
53    pub(super) fn sprite_quad(
54        out: &mut Vec<Quad>,
55        s: &SpriteRef,
56        rect: [i32; 4],
57        color: u32,
58        clip: Option<[i32; 4]>,
59    ) {
60        if rect[2] <= rect[0] || rect[3] <= rect[1] {
61            return;
62        }
63        out.push(Quad {
64            texture: Texture::Sheet(s.sheet),
65            rect: rect.map(|v| v as f32),
66            uv: s.uv,
67            color: argb_to_rgba(color, 255),
68            clip,
69            rotation: 0.0,
70        });
71    }
72
73    pub(super) fn draw_text(
74        &self,
75        t: &TextData,
76        origin: [i32; 2],
77        color: u32,
78        clip: Option<[i32; 4]>,
79        out: &mut Vec<Quad>,
80    ) {
81        let Some(font) = t.font else { return };
82        let Some(sheet) = self.res.fonts[font].sheet else {
83            return;
84        };
85        let sprites = &self.res.sheets[sheet].sheet.sprites;
86        if t.shadow {
87            let mut shadow = t.clone();
88            shadow.shadow = false;
89            self.draw_text(
90                &shadow,
91                [origin[0] + 1, origin[1] + 1],
92                color & 0xFF000000,
93                clip,
94                out,
95            );
96        }
97        for g in &t.layout.glyphs {
98            let Some(sp) = sprites.get(g.glyph) else {
99                continue;
100            };
101            let (x, y) = (origin[0] + g.x, origin[1] + g.y);
102            out.push(Quad {
103                texture: Texture::Sheet(sheet),
104                rect: [
105                    x as f32,
106                    y as f32,
107                    (x + sp.width) as f32,
108                    (y + sp.height) as f32,
109                ],
110                uv: sp.uv,
111                color: argb_to_rgba(color, 255),
112                clip,
113                rotation: 0.0,
114            });
115        }
116    }
117
118    pub(super) fn draw_node(
119        &self,
120        id: NodeId,
121        inherited: u32,
122        clip: Option<[i32; 4]>,
123        out: &mut Vec<Quad>,
124    ) {
125        let n = &self.nodes[id];
126        if !n.visible() {
127            return;
128        }
129        let mut color = modulate(n.color, inherited);
130        if n.flags & flags::DIM != 0 {
131            color = (color & 0x00FF_FFFF) | ((color >> 25) << 24);
132        }
133        let [l, t, r, b] = n.rect;
134        if let Some(border) = n.border {
135            self.draw_node(border, color, clip, out);
136        }
137        match &n.kind {
138            Kind::Control(c) => self.draw_control(id, c, color, clip, out),
139            Kind::Border { sprites } => {
140                let w = |i: usize| sprites[i].map_or(0, |s| s.width);
141                let h = |i: usize| sprites[i].map_or(0, |s| s.height);
142                let cells = [
143                    [l, t, l + w(0), t + h(0)],
144                    [l + w(0), t, r - w(2), t + h(1)],
145                    [r - w(2), t, r, t + h(2)],
146                    [l, t + h(0), l + w(3), b - h(6)],
147                    [l + w(3), t + h(1), r - w(5), b - h(7)],
148                    [r - w(5), t + h(2), r, b - h(8)],
149                    [l, b - h(6), l + w(6), b],
150                    [l + w(6), b - h(7), r - w(8), b],
151                    [r - w(8), b - h(8), r, b],
152                ];
153                for (s, rect) in sprites.iter().zip(cells) {
154                    if let Some(s) = s {
155                        Self::sprite_quad(out, s, rect, color, clip);
156                    }
157                }
158            }
159            Kind::Image {
160                sprite: Some(s), ..
161            } => {
162                Self::sprite_quad(out, s, n.rect, color, clip);
163                if n.rotation != 0.0 {
164                    if let Some(q) = out.last_mut() {
165                        q.rotation = n.rotation;
166                    }
167                }
168            }
169            Kind::Lines {
170                sprite: Some(s),
171                lines,
172                ..
173            } => {
174                // One-pixel steps along each segment, clipped to the element.
175                let r = n.rect;
176                let clip = Some(match clip {
177                    Some(c) => [
178                        c[0].max(r[0]),
179                        c[1].max(r[1]),
180                        c[2].min(r[2]),
181                        c[3].min(r[3]),
182                    ],
183                    None => r,
184                });
185                for &[x0, y0, x1, y1] in lines {
186                    let steps = (x1 - x0).abs().max((y1 - y0).abs()).max(1);
187                    for i in 0..=steps {
188                        let x = r[0] + x0 + (x1 - x0) * i / steps;
189                        let y = r[1] + y0 + (y1 - y0) * i / steps;
190                        Self::sprite_quad(out, s, [x, y, x + 1, y + 1], color, clip);
191                    }
192                }
193            }
194            Kind::AnimatedImage { sprites, frame, .. } => {
195                if let Some(Some(sprite)) = sprites.get(*frame) {
196                    Self::sprite_quad(out, sprite, n.rect, color, clip);
197                }
198            }
199            Kind::Button {
200                states,
201                content,
202                colors,
203                state,
204            } => {
205                // A button made unusable shows its disabled state at once, not only after the
206                // pointer next moves (`ButtonElement` draws from its enabled flag).
207                let state = &if n.usable() { *state } else { 1 };
208                if let Some(s) = states[*state].or(states[0]) {
209                    self.draw_node(s, color, clip, out);
210                }
211                if let Some(c) = content {
212                    let tint = if colors[*state] != 0 {
213                        modulate(colors[*state], color)
214                    } else {
215                        color
216                    };
217                    self.draw_node(*c, tint, clip, out);
218                }
219            }
220            Kind::Text(text) => {
221                let x = match text.justify {
222                    1 => l + ((r - l) - text.layout.width) / 2,
223                    2 => r - text.layout.width,
224                    _ => l,
225                };
226                self.draw_text(text, [x, t], color, clip, out);
227            }
228            Kind::Label {
229                text,
230                icon,
231                icon_size,
232                text_alignment,
233                orientation,
234                gap,
235                icon_color,
236                ..
237            } => {
238                let iw = if icon_size[0] > 0 {
239                    icon_size[0]
240                } else {
241                    icon.map_or(0, |s| s.width)
242                };
243                let ih = if icon_size[1] > 0 {
244                    icon_size[1]
245                } else {
246                    icon.map_or(0, |s| s.height)
247                };
248                let (tw, th) = (text.layout.width, text.layout.height);
249                // Areas for the text and the icon by orientation (LabelElement::Draw).
250                let (text_area, icon_pos) = match orientation {
251                    0 => ([l, t, r - gap - iw, b], [r - iw, t + ((b - t) - ih) / 2]),
252                    1 => ([l + iw + gap, t, r, b], [l, t + ((b - t) - ih) / 2]),
253                    3 => ([l, t + ih + gap, r, b], [l + ((r - l) - iw) / 2, t]),
254                    _ => ([l, t, r, b - ih - gap], [l + ((r - l) - iw) / 2, b - ih]),
255                };
256                let [al, at, ar, ab] = text_area;
257                let x = match text_alignment % 3 {
258                    1 => al + ((ar - al) - tw) / 2,
259                    2 => ar - tw,
260                    _ => al,
261                };
262                let y = match text_alignment / 3 {
263                    1 => at + ((ab - at) - th) / 2,
264                    2 => ab - th,
265                    _ => at,
266                };
267                if let Some(s) = icon {
268                    Self::sprite_quad(
269                        out,
270                        s,
271                        [icon_pos[0], icon_pos[1], icon_pos[0] + iw, icon_pos[1] + ih],
272                        *icon_color,
273                        clip,
274                    );
275                }
276                self.draw_text(text, [x, y], color, clip, out);
277            }
278            Kind::List(list) => {
279                let list_clip = controls::intersect(clip, [l, t, r, b]);
280                let first = (list.scroll / list.row_height.max(1)).max(0) as usize;
281                let rows = ((b - t) / list.row_height.max(1) + 2) as usize;
282                for i in first..(first + rows).min(list.items.len()) {
283                    let y = t + i as i32 * list.row_height - list.scroll;
284                    let row = [l, y, r, y + list.row_height];
285                    let tint = if list.selected == Some(i) {
286                        Some(0xC0C9_A24A)
287                    } else if list.hovered == Some(i) {
288                        Some(0x40FF_FFFF)
289                    } else {
290                        None
291                    };
292                    if let (Some(c), Some(s)) = (tint, &list.highlight) {
293                        Self::sprite_quad(out, s, row, c, list_clip);
294                    }
295                    let layout = match list.font {
296                        Some(f) => TextLayout::compute(&self.res, f, &list.items[i], None, 0),
297                        None => continue,
298                    };
299                    let text = TextData {
300                        font: list.font,
301                        text: String::new(),
302                        max_width: 0,
303                        justify: 0,
304                        wrap: false,
305                        shadow: false,
306                        ellipsis_width: 0,
307                        layout,
308                    };
309                    // Inferred: an item's sprite at the row's left, fitted to the row's
310                    // height, its text after it.
311                    let mut x = l + 6;
312                    if let Some(Some(s)) = list.icons.get(i) {
313                        let h = s.height.min(list.row_height).max(1);
314                        let w = (s.width * h / s.height.max(1)).max(1);
315                        let iy = y + (list.row_height - h) / 2;
316                        Self::sprite_quad(out, s, [x, iy, x + w, iy + h], color, list_clip);
317                        x += w + 4;
318                    }
319                    let ty = y + (list.row_height - text.layout.height) / 2;
320                    self.draw_text(&text, [x, ty], color, list_clip, out);
321                }
322                // Scroll indicator.
323                let total = list.items.len() as i32 * list.row_height;
324                if total > b - t {
325                    if let Some(s) = &list.highlight {
326                        let h = ((b - t) * (b - t) / total).max(12);
327                        let y = t + (list.scroll * ((b - t) - h)) / (total - (b - t)).max(1);
328                        Self::sprite_quad(out, s, [r - 4, y, r, y + h], 0x80FF_FFFF, list_clip);
329                    }
330                }
331            }
332            Kind::Picture { texture, size } if size[0] > 0 && size[1] > 0 => {
333                // Fit inside the rect, preserving aspect, never upscaling past 4x.
334                let (aw, ah) = ((r - l) as f32, (b - t) as f32);
335                let scale = (aw / size[0] as f32).min(ah / size[1] as f32).min(4.0);
336                let (w, h) = (size[0] as f32 * scale, size[1] as f32 * scale);
337                let (x, y) = (l as f32 + (aw - w) / 2.0, t as f32 + (ah - h) / 2.0);
338                out.push(Quad {
339                    texture: Texture::External(*texture),
340                    rect: [x, y, x + w, y + h],
341                    uv: [0.0, 0.0, 1.0, 1.0],
342                    color: [255, 255, 255, 255],
343                    clip,
344                    rotation: 0.0,
345                });
346            }
347            _ => {}
348        }
349        for &c in &n.children {
350            self.draw_node(c, color, clip, out);
351        }
352    }
353}