yakui_widgets/widgets/
render_textbox.rs1use std::cell::RefCell;
2use std::fmt;
3use std::rc::Rc;
4
5use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle as FontdueTextStyle};
6use yakui_core::geometry::{Color, Constraints, Rect, Vec2};
7use yakui_core::paint::PaintRect;
8use yakui_core::widget::{LayoutContext, PaintContext, Widget};
9use yakui_core::Response;
10
11use crate::font::Fonts;
12use crate::style::TextStyle;
13use crate::util::widget;
14
15use super::render_text::{get_text_layout_size, paint_text};
16
17#[derive(Debug, Clone)]
23#[non_exhaustive]
24#[must_use = "yakui widgets do nothing if you don't `show` them"]
25pub struct RenderTextBox {
26 pub text: String,
27 pub style: TextStyle,
28 pub selected: bool,
29 pub cursor: usize,
30}
31
32impl RenderTextBox {
33 pub fn new<S: Into<String>>(text: S) -> Self {
34 Self {
35 text: text.into(),
36 style: TextStyle::label(),
37 selected: false,
38 cursor: 0,
39 }
40 }
41
42 pub fn show(self) -> Response<RenderTextBoxResponse> {
43 widget::<RenderTextBoxWidget>(self)
44 }
45}
46
47pub struct RenderTextBoxWidget {
48 props: RenderTextBox,
49 cursor_pos_size: RefCell<(Vec2, f32)>,
50 layout: Rc<RefCell<Layout>>,
51}
52
53pub struct RenderTextBoxResponse {
54 pub layout: Rc<RefCell<Layout>>,
57}
58
59impl Widget for RenderTextBoxWidget {
60 type Props<'a> = RenderTextBox;
61 type Response = RenderTextBoxResponse;
62
63 fn new() -> Self {
64 let layout = Layout::new(CoordinateSystem::PositiveYDown);
65
66 Self {
67 props: RenderTextBox::new(""),
68 cursor_pos_size: RefCell::new((Vec2::ZERO, 0.0)),
69 layout: Rc::new(RefCell::new(layout)),
70 }
71 }
72
73 fn update(&mut self, props: Self::Props<'_>) -> Self::Response {
74 self.props = props;
75 RenderTextBoxResponse {
76 layout: self.layout.clone(),
77 }
78 }
79
80 fn layout(&self, ctx: LayoutContext<'_>, input: Constraints) -> Vec2 {
81 let fonts = ctx.dom.get_global_or_init(Fonts::default);
82 let font = match fonts.get(&self.props.style.font) {
83 Some(font) => font,
84 None => {
85 return input.min;
87 }
88 };
89
90 let text = &self.props.text;
91
92 let (max_width, max_height) = if input.is_bounded() {
93 (
94 Some(input.max.x * ctx.layout.scale_factor()),
95 Some(input.max.y * ctx.layout.scale_factor()),
96 )
97 } else {
98 (None, None)
99 };
100
101 let font_size = (self.props.style.font_size * ctx.layout.scale_factor()).ceil();
102
103 let mut text_layout = self.layout.borrow_mut();
104 text_layout.reset(&LayoutSettings {
105 max_width,
106 max_height,
107 ..LayoutSettings::default()
108 });
109 text_layout.append(&[&*font], &FontdueTextStyle::new(text, font_size, 0));
110
111 let lines = text_layout.lines().map(|x| x.as_slice()).unwrap_or(&[]);
112 let glyphs = text_layout.glyphs();
113
114 let cursor_x = if self.props.cursor >= self.props.text.len() {
118 text_layout
121 .glyphs()
122 .last()
123 .map(|glyph| glyph.x + glyph.width as f32 + 1.0)
124 } else {
125 text_layout.glyphs().iter().find_map(|glyph| {
128 if glyph.byte_offset != self.props.cursor {
129 return None;
130 }
131
132 Some(glyph.x - 2.0)
133 })
134 };
135
136 let cursor_line = lines
137 .iter()
138 .find(|line| {
139 let start_byte = glyphs[line.glyph_start].byte_offset;
140 let end_byte = glyphs[line.glyph_end].byte_offset;
141 self.props.cursor >= start_byte && self.props.cursor <= end_byte
142 })
143 .or_else(|| lines.last());
144 let cursor_y = cursor_line
145 .map(|line| line.baseline_y - line.max_ascent)
146 .unwrap_or(0.0);
147
148 let metrics = font.vertical_line_metrics(font_size);
149 let ascent = metrics.map(|m| m.ascent).unwrap_or(font_size) / ctx.layout.scale_factor();
150 let cursor_size = ascent;
151
152 let cursor_pos = Vec2::new(cursor_x.unwrap_or(0.0), cursor_y) / ctx.layout.scale_factor();
153 *self.cursor_pos_size.borrow_mut() = (cursor_pos, cursor_size);
154
155 let mut size = get_text_layout_size(&text_layout, ctx.layout.scale_factor());
156 size = size.max(Vec2::new(0.0, ascent));
157
158 input.constrain(size)
159 }
160
161 fn paint(&self, mut ctx: PaintContext<'_>) {
162 let text_layout = self.layout.borrow_mut();
163 let layout_node = ctx.layout.get(ctx.dom.current()).unwrap();
164
165 paint_text(
166 &mut ctx,
167 &self.props.style.font,
168 layout_node.rect.pos(),
169 &text_layout,
170 self.props.style.color,
171 );
172
173 if self.props.selected {
174 let (pos, size) = *self.cursor_pos_size.borrow();
175
176 let cursor_pos = layout_node.rect.pos() + pos;
177 let cursor_size = Vec2::new(1.0, size);
178
179 let mut rect = PaintRect::new(Rect::from_pos_size(cursor_pos, cursor_size));
180 rect.color = Color::RED;
181 rect.add(ctx.paint);
182 }
183 }
184}
185
186impl fmt::Debug for RenderTextBoxWidget {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 f.debug_struct("RenderTextBoxWidget")
189 .field("props", &self.props)
190 .field("layout", &"(no debug impl)")
191 .finish()
192 }
193}