shuyi_ui/dialogue/text_box/
glyph.rs1use super::{TextView, WaitView};
9use crate::ctx::{Ctx, GameData};
10use crate::frame::color::{EngineColor, Palette};
11use crate::frame::glyph_buffer::{BoxStyle, Cell, GlyphBuffer, Rect};
12use crate::prelude::*;
13use crate::widgets::menu::MenuView;
14
15const LOG_HINT_GAP: i32 = 3;
17
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum NarrationAlign {
23 #[default]
25 Centred,
26 Left,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46#[non_exhaustive]
47pub struct BoxPlace {
48 pub frame: Rect,
50 pub text_x: i32,
52 pub text_y: i32,
54 pub narration: NarrationAlign,
56}
57
58impl BoxPlace {
59 #[must_use]
63 pub const fn new(frame: Rect, text_x: i32, text_y: i32) -> Self {
64 Self {
65 frame,
66 text_x,
67 text_y,
68 narration: NarrationAlign::Centred,
69 }
70 }
71
72 #[must_use]
75 pub const fn with_narration(self, narration: NarrationAlign) -> Self {
76 Self { narration, ..self }
77 }
78}
79
80pub fn box_rect(place: BoxPlace, text: &TextView) -> (Rect, i32) {
84 let frame = place.frame;
85 let Some(choices) = &text.choices else {
86 return (frame, place.text_y);
87 };
88 let question = text.lines.len();
89 let gap = usize::from(question > 0);
90 let rows = i32::try_from(question + gap + choices.items.len()).unwrap_or(0);
91 let bottom = frame.y + frame.h - 1;
93 let text_y = (bottom - rows).min(place.text_y);
94 let top = text_y - (place.text_y - frame.y);
95 (Rect::new(frame.x, top, frame.w, bottom + 1 - top), text_y)
96}
97
98pub fn paint<G: GameData>(
102 ctx: &Ctx<G>,
103 buf: &mut GlyphBuffer,
104 place: BoxPlace,
105 text: &TextView,
106 speaker: Option<&str>,
107) {
108 let palette = &ctx.palette;
109 let c = |e| palette.engine(e);
110 let bg = c(EngineColor::Panel);
111 let (frame, text_y) = box_rect(place, text);
112 paint_frame(palette, buf, speaker, frame);
113
114 if let Some(skip) = &text.skip {
115 let (x, y) = (place.text_x, place.text_y);
116 buf.print(x, y, &skip.question, c(EngineColor::Text), bg);
117 buf.print(x, y + 1, &skip.answers, c(EngineColor::TextDim), bg);
118 return;
119 }
120
121 let alone = text.choices.is_none();
124 paint_lines(palette, buf, place, text, text_y, alone);
125 if let Some(choices) = &text.choices {
126 let gap = i32::from(!text.lines.is_empty());
127 let first = text_y + i32::try_from(text.lines.len()).unwrap_or(0) + gap;
128 paint_choices(ctx, buf, place.text_x, choices, first);
129 }
130 if let Some(waiting) = &text.waiting {
131 paint_waiting(palette, buf, place.frame, waiting);
132 }
133}
134
135fn paint_frame(palette: &Palette, buf: &mut GlyphBuffer, speaker: Option<&str>, frame: Rect) {
137 let c = |e| palette.engine(e);
138 let bg = c(EngineColor::Panel);
139 buf.fill_rect(frame, Cell::new(' ', c(EngineColor::Text), bg));
140 buf.draw_box(frame, BoxStyle::Single, c(EngineColor::Border), bg);
141 if let Some(speaker) = speaker {
142 let name = format!(" {speaker} ");
143 let fg = c(EngineColor::Highlight);
144 buf.print(frame.x + 3, frame.y, &name, fg, bg);
145 }
146}
147
148fn paint_lines(
153 palette: &Palette,
154 buf: &mut GlyphBuffer,
155 place: BoxPlace,
156 text: &TextView,
157 text_y: i32,
158 alone: bool,
159) {
160 let c = |e| palette.engine(e);
161 let bg = c(EngineColor::Panel);
162 let narration = text.narration;
163 let fg = if narration {
164 c(EngineColor::TextDim)
165 } else {
166 c(EngineColor::Text)
167 };
168 let centred = narration && place.narration == NarrationAlign::Centred;
169 let top = text_y + i32::from(centred && text.lines.len() == 1 && alone);
170 for ((y, line), visible) in (top..).zip(&text.lines).zip(text.revealed()) {
171 let x = if centred {
172 let len = buf.text_cells(line);
173 place.frame.x + (place.frame.w - len) / 2
174 } else {
175 place.text_x
176 };
177 buf.print(x, y, &visible, fg, bg);
178 }
179}
180
181fn paint_choices<G: GameData>(
184 ctx: &Ctx<G>,
185 buf: &mut GlyphBuffer,
186 x: i32,
187 choices: &MenuView,
188 first: i32,
189) {
190 let c = |e| ctx.palette.engine(e);
191 let bg = c(EngineColor::Panel);
192 for (y, item) in (first..).zip(&choices.items) {
193 let (marker, fg) = if item.focused {
194 ("> ", c(EngineColor::Highlight))
195 } else {
196 (" ", c(EngineColor::Text))
197 };
198 let label = ctx.label(&item.label);
199 buf.print(x, y, &format!("{marker}{label}"), fg, bg);
200 }
201}
202
203fn paint_waiting(palette: &Palette, buf: &mut GlyphBuffer, frame: Rect, waiting: &WaitView) {
206 let c = |e| palette.engine(e);
207 let bg = c(EngineColor::Panel);
208 let right = frame.x + frame.w - 4;
209 let bottom = frame.y + frame.h - 2;
210 let key = &waiting.key;
211 let w = buf.text_cells(key);
212 buf.print(right - 1 - w, bottom, key, c(EngineColor::TextDim), bg);
213 if let Some(hint) = &waiting.log_hint {
215 let hint_w = buf.text_cells(hint);
216 let x = right - 1 - w - LOG_HINT_GAP - hint_w;
217 buf.print(x, bottom, hint, c(EngineColor::TextDim), bg);
218 }
219 if waiting.marker {
220 buf.print(right, bottom, "▼", c(EngineColor::Highlight), bg);
221 }
222}
223
224#[cfg(test)]
225mod tests;