veloren_voxygen/ui/ice/renderer/widget/text_input.rs
1use super::super::{super::FontId, IcedRenderer, Primitive};
2use glyph_brush::GlyphCruncher;
3use iced::{
4 Color, Point, Rectangle, mouse,
5 text_input::{self, cursor},
6};
7
8const CURSOR_WIDTH: f32 = 2.0;
9// Extra scroll offset past the cursor
10const EXTRA_OFFSET: f32 = 10.0;
11
12impl text_input::Renderer for IcedRenderer {
13 type Style = ();
14
15 fn measure_value(&self, value: &str, size: u16, font: Self::Font) -> f32 {
16 // Using the physical scale might make this cached info usable below?
17 // Although we also have a position of the screen there so this could be useless
18 let p_scale = self.p_scale;
19
20 let section = glyph_brush::Section {
21 screen_position: (0.0, 0.0),
22 bounds: (f32::INFINITY, f32::INFINITY),
23 layout: Default::default(),
24 text: vec![glyph_brush::Text {
25 text: value,
26 scale: (size as f32 * p_scale).into(),
27 font_id: font.0,
28 extra: (),
29 }],
30 };
31
32 let mut glyph_calculator = self.cache.glyph_calculator();
33 // Note: keeping comments below for now in case this needs to be debugged again
34 /* let width = */
35 glyph_calculator
36 .glyph_bounds(section)
37 .map_or(0.0, |rect| rect.width() / p_scale)
38
39 // glyph_brush ignores the exterior spaces
40 // or does it!!!
41 // TODO: need better layout lib
42 /*let exterior_spaces = value.len() - value.trim().len();
43
44 if exterior_spaces > 0 {
45 use glyph_brush::ab_glyph::{Font, ScaleFont};
46 // Could cache this if it is slow
47 let sur = format!("x{}x", value);
48 let section = glyph_brush::Section {
49 screen_position: (0.0, 0.0),
50 bounds: (f32::INFINITY, f32::INFINITY),
51 layout: Default::default(),
52 text: vec![glyph_brush::Text {
53 text: &sur,
54 scale: (size as f32 * p_scale).into(),
55 font_id: font.0,
56 extra: (),
57 }],
58 i;
59 let font = glyph_calculator.fonts()[font.0].as_scaled(size as f32);
60 let space_id = font.glyph_id(' ');
61 let x_id = font.glyph_id('x');
62 let space_width = font.h_advance(space_id);
63 let x_width = font.h_advance(x_id);
64 let kern1 = font.kern(x_id, space_id);
65 let kern2 = font.kern(space_id, x_id);
66 dbg!(font.kern(x_id, x_id));
67 let sur_width = glyph_calculator
68 .glyph_bounds(section)
69 .map_or(0.0, |rect| rect.width() / p_scale);
70 dbg!(space_width);
71 dbg!(width);
72 dbg!(sur_width);
73 let extra = x_width * 2.0 + dbg!(kern1) + dbg!(kern2);
74 dbg!(extra);
75 dbg!(sur_width - extra);
76 width += exterior_spaces as f32 * space_width;
77 }*/
78
79 //width
80 }
81
82 fn offset(
83 &self,
84 text_bounds: Rectangle,
85 font: Self::Font,
86 size: u16,
87 value: &text_input::Value,
88 state: &text_input::State,
89 ) -> f32 {
90 // Only need to offset if focused with cursor somewhere in the text
91 if state.is_focused() {
92 let cursor = state.cursor();
93
94 let focus_position = match cursor.state(value) {
95 cursor::State::Index(i) => i,
96 cursor::State::Selection { end, .. } => end,
97 };
98
99 let (_, offset) = measure_cursor_and_scroll_offset(
100 self,
101 text_bounds,
102 value,
103 size,
104 focus_position,
105 font,
106 );
107
108 offset
109 } else {
110 0.0
111 }
112 }
113
114 fn draw(
115 &mut self,
116 bounds: Rectangle,
117 text_bounds: Rectangle,
118 //defaults: &Self::Defaults, No defaults!!
119 cursor_position: Point,
120 font: Self::Font,
121 size: u16,
122 placeholder: &str,
123 value: &text_input::Value,
124 state: &text_input::State,
125 _style_sheet: &Self::Style,
126 ) -> Self::Output {
127 let is_mouse_over = bounds.contains(cursor_position);
128
129 // Note: will be useful in the future if we vary the style with the state of the
130 // text input
131 /*
132 let style = if state.is_focused() {
133 style.focused()
134 } else if is_mouse_over {
135 style.hovered()
136 } else {
137 style.active()
138 }; */
139
140 let p_scale = self.p_scale;
141
142 // Allocation :(
143 let text = value.to_string();
144 let text = if !text.is_empty() { Some(&*text) } else { None };
145
146 // TODO: background from style, image?
147
148 // TODO: color from style
149 let color = if text.is_some() {
150 Color::WHITE
151 } else {
152 Color::from_rgba(1.0, 1.0, 1.0, 0.3)
153 };
154 let linear_color = color.into_linear().into();
155
156 let (cursor_primitive, scroll_offset) = if state.is_focused() {
157 let cursor = state.cursor();
158
159 let cursor_and_scroll_offset = |position| {
160 measure_cursor_and_scroll_offset(self, text_bounds, value, size, position, font)
161 };
162
163 let (cursor_primitive, offset) = match cursor.state(value) {
164 cursor::State::Index(position) => {
165 let (position, offset) = cursor_and_scroll_offset(position);
166 (
167 Primitive::Rectangle {
168 bounds: Rectangle {
169 x: text_bounds.x + position - CURSOR_WIDTH / p_scale / 2.0,
170 y: text_bounds.y,
171 width: CURSOR_WIDTH / p_scale,
172 height: text_bounds.height,
173 },
174 linear_color,
175 },
176 offset,
177 )
178 },
179 cursor::State::Selection { start, end } => {
180 let left = start.min(end);
181 let right = end.max(start);
182
183 let (left_position, left_offset) = cursor_and_scroll_offset(left);
184 let (right_position, right_offset) = cursor_and_scroll_offset(right);
185
186 let offset = if end == right {
187 right_offset
188 } else {
189 left_offset
190 };
191 let width = right_position - left_position;
192
193 (
194 Primitive::Rectangle {
195 bounds: Rectangle {
196 x: text_bounds.x + left_position,
197 y: text_bounds.y,
198 width,
199 height: text_bounds.height,
200 },
201 // TODO: selection color from stlye
202 linear_color: Color::from_rgba(1.0, 0.0, 1.0, 0.2).into_linear().into(),
203 },
204 offset,
205 )
206 },
207 };
208
209 (Some(cursor_primitive), offset)
210 } else {
211 (None, 0.0)
212 };
213
214 let display_text = text.unwrap_or(if state.is_focused() { "" } else { placeholder });
215 // Note: clip offset is an integer so we don't have to worry about not
216 // accounting for that here where the alignment of the glyphs with
217 // pixels affects rasterization
218 let glyphs = self.position_glyphs(
219 Rectangle {
220 width: 1000.0, // hacky
221 ..bounds
222 },
223 iced::HorizontalAlignment::Left,
224 iced::VerticalAlignment::Center,
225 display_text,
226 size,
227 font,
228 );
229
230 let text_primitive = Primitive::Text {
231 glyphs,
232 bounds,
233 linear_color,
234 };
235
236 let primitive = match cursor_primitive {
237 Some(cursor_primitive) => Primitive::Group {
238 primitives: vec![cursor_primitive, text_primitive],
239 },
240 None => text_primitive,
241 };
242
243 // Probably already computed this somewhere
244 let text_width = self.measure_value(display_text, size, font);
245
246 let primitive = if text_width > text_bounds.width {
247 Primitive::Clip {
248 bounds: text_bounds,
249 offset: (scroll_offset as u32, 0).into(),
250 content: Box::new(primitive),
251 }
252 } else {
253 primitive
254 };
255
256 (
257 primitive,
258 if is_mouse_over {
259 mouse::Interaction::Text
260 } else {
261 mouse::Interaction::default()
262 },
263 )
264 }
265}
266
267fn measure_cursor_and_scroll_offset(
268 renderer: &IcedRenderer,
269 text_bounds: Rectangle,
270 value: &text_input::Value,
271 size: u16,
272 cursor_index: usize,
273 font: FontId,
274) -> (f32, f32) {
275 use text_input::Renderer;
276
277 // TODO: so much allocation (fyi .until() allocates)
278 let text_before_cursor = value.until(cursor_index).to_string();
279
280 let text_value_width = renderer.measure_value(&text_before_cursor, size, font);
281 let offset = ((text_value_width + EXTRA_OFFSET) - text_bounds.width).max(0.0);
282
283 (text_value_width, offset)
284}