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}