all_is_cubes_ui/vui/widgets/
text.rs1use all_is_cubes::{listen, universe};
2use alloc::boxed::Box;
3use alloc::sync::Arc;
4
5use all_is_cubes::arcstr::ArcStr;
6use all_is_cubes::block::text::{self, Text as BlockText};
7use all_is_cubes::block::{self, Resolution::*};
8use all_is_cubes::drawing::embedded_graphics::{
9 Drawable,
10 mono_font::MonoTextStyle,
11 prelude::{Dimensions, Point},
12 text::{Text as EgText, TextStyle},
13};
14use all_is_cubes::drawing::{VoxelBrush, rectangle_to_aab};
15use all_is_cubes::math::{GridAab, GridSize, Gridgid};
16use all_is_cubes::space::{CubeTransaction, SpaceTransaction};
17
18use crate::vui::{self, LayoutGrant, LayoutRequest, Layoutable, Widget, WidgetController, widgets};
19
20#[derive(Clone, Debug)]
28#[expect(clippy::exhaustive_structs)] pub struct LargeText {
30 pub text: ArcStr,
32 pub font: text::Font,
34 pub brush: VoxelBrush<'static>,
36 pub text_style: TextStyle,
38}
39
40impl LargeText {
41 fn drawable(&self) -> EgText<'_, MonoTextStyle<'_, &VoxelBrush<'_>>> {
42 EgText::with_text_style(
43 &self.text,
44 Point::new(0, 0),
45 MonoTextStyle::new(self.font.eg_font(), &self.brush),
46 self.text_style,
47 )
48 }
49
50 fn bounds(&self) -> GridAab {
51 rectangle_to_aab(
53 self.drawable().bounding_box(),
54 Gridgid::FLIP_Y,
55 self.brush.bounds().unwrap_or(GridAab::ORIGIN_CUBE),
56 )
57 }
58}
59
60impl Layoutable for LargeText {
61 fn requirements(&self) -> LayoutRequest {
62 LayoutRequest {
63 minimum: self.bounds().size(),
64 }
65 }
66}
67
68impl Widget for LargeText {
69 fn controller(self: Arc<Self>, position: &LayoutGrant) -> Box<dyn WidgetController> {
70 let mut txn = SpaceTransaction::default();
71 let drawable = self.drawable();
72 let draw_bounds = self.bounds();
73 drawable
74 .draw(&mut txn.draw_target(
75 Gridgid::from_translation(
76 position.shrink_to(draw_bounds.size(), false).bounds.lower_bounds()
77 - draw_bounds.lower_bounds(),
78 ) * Gridgid::FLIP_Y,
79 ))
80 .unwrap();
81
82 widgets::OneshotController::new(txn)
83 }
84}
85
86#[derive(Clone, Debug, Eq, Hash, PartialEq)]
94pub struct Label {
95 text: ArcStr,
96 font: text::Font,
97 positioning: Option<text::Positioning>,
98}
99
100impl Label {
101 pub fn new(string: ArcStr) -> Self {
103 Self {
104 text: string,
105 font: text::Font::System16,
106 positioning: None,
107 }
108 }
109
110 #[cfg_attr(not(feature = "session"), expect(dead_code))]
114 pub(crate) fn with_font(
115 string: ArcStr,
116 font: text::Font,
117 positioning: text::Positioning,
118 ) -> Self {
119 Self {
120 text: string,
121 font,
122 positioning: Some(positioning),
123 }
124 }
125
126 pub(crate) fn text(&self, gravity: vui::Gravity) -> text::Text {
128 text_for_widget(
129 self.text.clone(),
130 self.font.clone(),
131 self.positioning.unwrap_or_else(|| gravity_to_positioning(gravity, true)),
132 )
133 }
134}
135
136impl Layoutable for Label {
137 fn requirements(&self) -> LayoutRequest {
138 LayoutRequest {
149 minimum: self.text(vui::Gravity::splat(vui::Align::Low)).bounding_blocks().size(),
150 }
151 }
152}
153
154impl Widget for Label {
155 fn controller(self: Arc<Self>, grant: &LayoutGrant) -> Box<dyn WidgetController> {
156 widgets::OneshotController::new(draw_text_txn(&self.text(grant.gravity), grant, true))
160 }
161}
162
163impl From<ArcStr> for Label {
164 fn from(value: ArcStr) -> Self {
166 Self::new(value)
167 }
168}
169
170impl universe::VisitHandles for Label {
171 fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
172 let Self {
173 text,
174 font,
175 positioning: _,
176 } = self;
177 text.visit_handles(visitor);
178 font.visit_handles(visitor);
179 }
180}
181
182#[derive(Clone, Debug)]
194pub struct TextBox {
195 text_source: listen::DynSource<ArcStr>,
196 font: text::Font,
197 positioning: Option<text::Positioning>,
198 minimum_size: GridSize,
200 }
202
203impl TextBox {
204 pub fn dynamic_label(text_source: listen::DynSource<ArcStr>, minimum_size: GridSize) -> Self {
207 Self {
208 text_source,
209 font: text::Font::System16,
210 positioning: None,
211 minimum_size,
212 }
213 }
214}
215
216impl Layoutable for TextBox {
217 fn requirements(&self) -> LayoutRequest {
218 LayoutRequest {
219 minimum: self.minimum_size,
220 }
221 }
222}
223
224impl Widget for TextBox {
225 fn controller(self: Arc<Self>, grant: &LayoutGrant) -> Box<dyn WidgetController> {
226 Box::new(TextBoxController {
227 todo: listen::Flag::listening(false, &self.text_source),
228 grant: *grant,
229 definition: self,
230 })
231 }
232}
233
234impl universe::VisitHandles for TextBox {
235 fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
236 let Self {
237 text_source: _,
238 font,
239 positioning: _,
240 minimum_size: _,
241 } = self;
242 font.visit_handles(visitor);
243 }
244}
245
246#[derive(Debug)]
248struct TextBoxController {
249 definition: Arc<TextBox>,
250 todo: listen::Flag,
251 grant: LayoutGrant,
252}
253
254impl TextBoxController {
255 fn draw_txn(&self) -> vui::WidgetTransaction {
256 draw_text_txn(
257 &text_for_widget(
258 self.definition.text_source.get(),
259 self.definition.font.clone(),
260 self.definition
261 .positioning
262 .unwrap_or_else(|| gravity_to_positioning(self.grant.gravity, true)),
263 ),
264 &self.grant,
265 false, )
267 }
268}
269
270impl WidgetController for TextBoxController {
271 fn initialize(
272 &mut self,
273 _: &vui::WidgetContext<'_, '_>,
274 ) -> Result<vui::WidgetTransaction, vui::InstallVuiError> {
275 Ok(self.draw_txn())
276 }
277
278 fn step(&mut self, _: &vui::WidgetContext<'_, '_>) -> Result<vui::StepSuccess, vui::StepError> {
279 Ok((
280 if self.todo.get_and_clear() {
281 self.draw_txn()
282 } else {
283 SpaceTransaction::default()
284 },
285 vui::Then::Step,
287 ))
288 }
289}
290
291impl universe::VisitHandles for TextBoxController {
292 fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
293 let Self {
294 definition,
295 todo: _,
296 grant: _,
297 } = self;
298 definition.visit_handles(visitor);
299 }
300}
301
302fn text_for_widget(text: ArcStr, font: text::Font, positioning: text::Positioning) -> text::Text {
305 text::Text::builder()
306 .resolution(R32)
307 .string(text)
308 .font(font)
309 .positioning(positioning)
310 .build()
311}
312
313fn gravity_to_positioning(gravity: vui::Gravity, ignore_y: bool) -> text::Positioning {
314 text::Positioning {
315 x: match gravity.x {
316 vui::Align::Low => text::PositioningX::Left,
317 vui::Align::Center => text::PositioningX::Center,
318 vui::Align::High => text::PositioningX::Right,
319 },
320 line_y: if ignore_y {
321 text::PositioningY::BodyMiddle
322 } else {
323 match gravity.y {
324 vui::Align::Low => text::PositioningY::BodyBottom,
325 vui::Align::Center => text::PositioningY::BodyMiddle,
326 vui::Align::High => text::PositioningY::BodyTop,
327 }
328 },
329 z: match gravity.z {
330 vui::Align::Low | vui::Align::Center => text::PositioningZ::Back,
331 vui::Align::High => text::PositioningZ::Front,
332 },
333 }
334}
335
336pub(crate) fn draw_text_txn(
341 text: &BlockText,
342 full_grant: &LayoutGrant,
343 shrink: bool,
344) -> SpaceTransaction {
345 let text_aabb = text.bounding_blocks();
346 let shrunk_grant = full_grant.shrink_to(text_aabb.size(), true);
347 let translation = shrunk_grant.bounds.lower_bounds() - text_aabb.lower_bounds();
348
349 SpaceTransaction::filling(
353 if shrink {
354 shrunk_grant.bounds
355 } else {
356 full_grant.bounds
357 },
358 |cube| {
359 let block = if !shrink && !shrunk_grant.bounds.contains_cube(cube) {
360 block::AIR
362 } else {
363 block::Block::from_primitive(block::Primitive::Text {
364 text: text.clone(),
365 offset: cube.lower_bounds().to_vector() - translation,
366 })
367 };
368 CubeTransaction::replacing(None, Some(block))
369 },
370 )
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use all_is_cubes::arcstr::literal;
377 use all_is_cubes::block::text::Font;
378 use all_is_cubes::euclid::size3;
379 use all_is_cubes::math::{GridSizeCoord, Rgba};
380 use all_is_cubes::space::{self, SpacePhysics};
381 use all_is_cubes::universe::ReadTicket;
382
383 #[test]
384 fn large_text_size() {
385 let text = "abc";
386 let widget = LargeText {
387 text: text.into(),
388 font: Font::Logo,
389 brush: VoxelBrush::single(block::from_color!(Rgba::WHITE)),
390 text_style: TextStyle::default(),
391 };
392 assert_eq!(
393 widget.requirements(),
394 LayoutRequest {
395 minimum: size3(9 * GridSizeCoord::try_from(text.len()).unwrap(), 15, 1)
396 }
397 );
398 }
399
400 #[test]
401 fn label_layout() {
402 let tree: vui::WidgetTree = vui::leaf_widget(Label::new(literal!("hi")));
403
404 tree.to_space(
406 ReadTicket::stub(),
407 space::Builder::default().physics(SpacePhysics::DEFAULT_FOR_BLOCK),
408 vui::Gravity::new(vui::Align::Center, vui::Align::Center, vui::Align::Low),
409 )
410 .unwrap();
411 }
412
413 #[test]
414 fn label_requirements() {
415 let string = literal!("abcdef");
417
418 let tree = vui::leaf_widget(Label::new(string));
419
420 assert_eq!(
422 tree.requirements(),
423 LayoutRequest {
424 minimum: size3(2, 1, 1)
425 }
426 );
427 }
428}