rust_widgets 2.7.0

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! SVG rendering trait for widgets.
//!
//! # Two Approaches
//!
//! 1. **Pipeline-accurate SVG** — Use [`render_widget_to_svg()`] which routes
//!    through the actual [`Draw::draw()`] pipeline via [`SvgPaintBackend`].
//!    The SVG output is guaranteed to match the widget's real rendering.
//!
//! Most users should use [`render_widget_to_svg()`] as it is zero-maintenance
//! and guaranteed accurate.

use crate::compat::{String, Vec};
use crate::core::{Rect, Size};
use crate::render::{PaintBackend, RenderContext, SvgPaintBackend};
use crate::widget::{Draw, Widget};

/// Render any widget to an SVG string using its [`Draw`] implementation.
///
/// This is the **recommended** way to generate SVG output — it routes through
/// the actual rendering pipeline via [`SvgPaintBackend`], so the SVG
/// matches the widget's real pixel output exactly.
///
/// # Usage
/// ```text
/// // render_widget_to_svg takes a `&mut impl Draw` and a geometry `Rect`:
/// let svg = render_widget_to_svg(&mut my_widget, Rect::new(0, 0, 100, 50));
/// ```
/// Convenience wrapper that auto-detects widget geometry.
/// Requires the widget to implement both [`Draw`] and [`Widget`].
pub fn render_to_svg<W: Draw + Widget>(widget: &mut W) -> String {
    let geom = widget.geometry();
    render_widget_to_svg(widget, geom)
}

/// Render any widget to an SVG string using its [`Draw`] implementation.
///
/// This is the **recommended** way to generate SVG output — it routes through
/// the actual rendering pipeline via [`SvgPaintBackend`], so the SVG
/// matches the widget's real pixel output exactly.
///
/// # Usage
/// ```text
/// // render_widget_to_svg takes a `&mut impl Draw` and a geometry `Rect`:
/// let svg = render_widget_to_svg(&mut my_widget, Rect::new(0, 0, 100, 50));
/// ```
pub fn render_widget_to_svg<T: Draw + ?Sized>(widget: &mut T, geometry: Rect) -> String {
    render_widget_to_svg_on(widget, geometry, crate::core::Color::WHITE)
}

/// Renders any widget to an SVG string, composited over `backdrop`.
///
/// # Why the backdrop is a parameter
///
/// A control is not obliged to paint a background: a `Label`, a `Separator` and several
/// others are **transparent by design**, because in a real window the surface behind them is
/// whatever their parent painted. Always filling the frame with `WHITE` therefore produced a
/// misleading picture for exactly those controls — the exporter applied the *dark* theme to a
/// label and then laid its near-white ink on white, so `<name>.svg` showed a blank rectangle
/// and `<name>.light.svg` showed the same control looking correct. The snapshot was wrong, not
/// the control.
///
/// Passing the active theme's background puts a transparent control on the surface it would
/// actually sit on, which is the picture a reviewer needs and the one P5's element bounds are
/// already measured against.
pub fn render_widget_to_svg_on<T: Draw + ?Sized>(
    widget: &mut T,
    geometry: Rect,
    backdrop: crate::core::Color,
) -> String {
    let size = Size::new(geometry.width, geometry.height);
    let mut backend = SvgPaintBackend::new(size);
    backend.begin_frame(backdrop);
    let mut ctx = RenderContext::new(&mut backend);
    widget.draw(&mut ctx);
    backend.end_frame();
    backend.finish()
}

/// The ink box of the first text run in an SVG document, as `(left, top, right, bottom)`.
///
/// This is the geometry-side entry point for a test that wants to check where text was drawn.
/// Together with [`text_subpath_count`] it replaces the `svg.contains("OK")` / `line.contains("<text")`
/// assertions the crate used before text became geometry: the string is no longer in the
/// document in any form, so a caller must either measure the ink ([`text_ink_box`]) or count it
/// ([`text_subpath_count`]).
///
/// # Why an ink box and not a single coordinate
///
/// Text leaves the renderer as **glyph geometry**, not as a `<text>` element: the SVG backend
/// emits the same `font8x8` rectangles the software rasteriser fills (see
/// `SvgPaintBackend::execute_command`, `RenderCommand::DrawText`). There is therefore no
/// `y` to read and no baseline to subtract — the document simply contains the drawing, and the
/// only way to interrogate it is geometrically.
///
/// This existed as `text_top_of`, which undid the `origin.y + ascent` baseline conversion the
/// backend used to perform for a `<text>` element. That conversion is gone: it was needed only
/// because the viewer's font engine, not this crate's, rasterised the string, so the two
/// renderers disagreed about where the ink was. Asserting against the emitted geometry is now
/// both possible and *stronger*: it pins the position **and** the extent, where a baseline
/// assertion pinned a quantity that did not exist in the output at all.
///
/// Text runs are emitted as a single `<path>` whose `d` holds one axis-aligned subpath per set
/// bitmap bit (`M{x0} {y0}h{w}v{h}h-{w}z`), so the union of those subpaths is exactly the ink.
///
/// Returns `None` for a document with no text path, or one whose `d` cannot be parsed.
///
/// ```text
/// let (left, top, _, _) = text_ink_box(&svg).expect("a text path");
/// assert_eq!(top, expected_glyph_box_top);
/// ```
pub fn text_ink_box(svg: &str) -> Option<(i32, i32, i32, i32)> {
    text_ink_boxes(svg).first().copied()
}

/// The ink box of **every** text run in an SVG document, in document order.
///
/// # Why `text_ink_box` was not enough
///
/// One `draw_text` call emits one `<path>`, so a control that paints two strings — a field with a unit
/// mark and a value, a header with a sort indicator and a label, a support row with a message and a
/// counter — produces two paths. [`text_ink_box`] returns only the first, which silently answers "where
/// is the first thing that was written" rather than "where is the thing I meant". A test that compares
/// two runs, or looks for the *second* one, cannot be expressed with the single-run form at all and
/// ends up asserting against whichever run happened to be painted first.
///
/// Returning all of them makes the run under test *selectable*, so a two-run assertion can say which
/// run it means instead of depending on paint order.
///
/// A control that paints nothing yields an empty list; the caller decides whether that is a failure.
///
/// # Non-text paths are excluded
///
/// A `font8x8` run is emitted as **one** `<path>` per `draw_text` call whose `d` is a run of
/// axis-aligned rectangles (`M{x0} {y0}h{w}v{h}h-{w}z`). An icon, an arrow head or an arbitrary
/// shape is also a `<path>`, and its `d` is a run of `M`/`L`/`C` commands. Both parse as "a path
/// with bounds", so without a discriminator this function answered "the bounds of the first path in
/// the document" — which is the arrow head of a popover, or a chevron, whenever one is painted
/// before the first string. The discriminator is the **command set**: text is built from `h`/`v`
/// only, so a `d` containing `L`, `C`, `Q` or `A` is not a text run and is skipped. A text path is
/// additionally required to hold at least one complete `h`/`v` pair, so a one-command `d` cannot
/// masquerade as a glyph.
pub fn text_ink_boxes(svg: &str) -> Vec<(i32, i32, i32, i32)> {
    let mut boxes = Vec::new();
    for line in svg.lines() {
        if !line.contains("<path") {
            continue;
        }
        let Some(d) = attribute_str(line, "d") else {
            continue;
        };
        if !is_text_path(d) {
            continue;
        }
        if let Some(bounds) = path_bounds(d) {
            boxes.push(bounds);
        }
    }
    boxes
}

/// Whether a `<path d>` is a texture of `font8x8` glyph rectangles rather than a drawn shape.
///
/// See [`text_ink_boxes`] for why the distinction has to exist. Glyph rectangles close with `z` and
/// step with `h`/`v`, in absolute or relative form; every other command (`L`, `l`, `C`, `c`, `Q`,
/// `q`, `A`, `a`, `S`, `s`, `T`, `t`, `H`, `V`) means the path is a picture, not a run of text.
fn is_text_path(d: &str) -> bool {
    let mut closed = 0usize;
    let mut stepped = 0usize;
    for byte in d.bytes() {
        match byte {
            b'h' | b'v' | b'H' | b'V' => stepped += 1,
            b'z' | b'Z' => closed += 1,
            // Any of these is a vertex command, which a glyph bitmap never emits.
            b'L' | b'l' | b'C' | b'c' | b'Q' | b'q' | b'A' | b'a' | b'S' | b's' | b'T' | b't' => {
                return false
            }
            _ => {}
        }
    }
    // A glyph rectangle is `h` then `v`; every subpath closes. Requiring both makes an empty or
    // single-command `d` answer "not text" rather than "a run with one pixel of ink".
    stepped >= 2 && closed >= 1
}

/// The number of **subpaths** in the document's text paths.
///
/// One subpath is one set bit of one glyph bitmap, so this is a direct measure of how much ink
/// the string laid down — enough to tell `"OK"` from `""`, or to notice that a longer string
/// produced no more ink than a shorter one (which is what a lost advance looks like).
///
/// A caller that wants "this string was drawn at all" should prefer this to
/// `svg.contains("OK")`: the string is no longer in the document in any form.
pub fn text_subpath_count(svg: &str) -> usize {
    svg.lines()
        .filter(|line| line.contains("<path"))
        .filter_map(|line| attribute_str(line, "d"))
        .map(|d| d.matches('M').count())
        .sum()
}

/// The bounds `(left, top, right, bottom)` of the document's first **shape** path, or `None`.
///
/// # Why this is public and `path_bounds` is not
///
/// [`text_ink_box`] answers "where is the ink of the first *text run*". A control that draws a
/// **shape** as a path — a triangle, a diamond, an elbow — has no way to ask where that shape landed,
/// so a test for it could only assert that the document *contains* a `<path>`, which is satisfied by
/// a path of any size at any position, including one that paints nothing. Knocking the shape out
/// left exactly such a test green.
///
/// "The first shape path" is the counterpart of "the first text path", and the two are disjoint by
/// [`is_text_path`], so a document cannot answer both for the same element. The raw parser stays
/// private: this is the narrow, named question, and it hands back the shape's bounds rather than an
/// unparsed `d`.
pub fn first_shape_bounds(svg: &str) -> Option<(i32, i32, i32, i32)> {
    svg.lines()
        .filter(|line| line.contains("<path"))
        .filter_map(|line| attribute_str(line, "d"))
        .filter(|d| !is_text_path(d))
        .find_map(path_bounds)
}

/// The bounds of an axis-aligned `<path d>` made of `M x y h w v h h -w z` subpaths.
///
/// Unknown commands are ignored rather than misread: a `d` this cannot understand yields the
/// bounds of the parts it does, and a `d` with nothing understood yields `None`.
fn path_bounds(d: &str) -> Option<(i32, i32, i32, i32)> {
    let bytes = d.as_bytes();
    let mut i = 0usize;
    let mut bounds: Option<(i32, i32, i32, i32)> = None;
    let mut cursor: Option<(f32, f32)> = None;
    while i < bytes.len() {
        let command = bytes[i];
        i += 1;
        match command {
            b'M' | b'L' => {
                let (x, y, next) = number_pair(d, i)?;
                i = next;
                cursor = Some((x, y));
                include(&mut bounds, x, y);
            }
            b'm' | b'l' => {
                // A relative move is relative to the current point, so the cursor is required — a
                // document starting with `m` would be malformed, and `?` reports that rather than
                // treating the delta as absolute.
                let (dx, dy, next) = number_pair(d, i)?;
                i = next;
                let (x, y) = cursor?;
                let point = (x + dx, y + dy);
                cursor = Some(point);
                include(&mut bounds, point.0, point.1);
            }
            b'h' | b'v' => {
                let (delta, next) = number(d, i)?;
                i = next;
                let (x, y) = cursor?;
                let point = if command == b'h' { (x + delta, y) } else { (x, y + delta) };
                cursor = Some(point);
                include(&mut bounds, point.0, point.1);
            }
            b'H' | b'V' => {
                let (value, next) = number(d, i)?;
                i = next;
                let (x, y) = cursor?;
                let point = if command == b'H' { (value, y) } else { (x, value) };
                cursor = Some(point);
                include(&mut bounds, point.0, point.1);
            }
            b'z' | b'Z' | b' ' | b',' | b'\t' | b'\n' => {}
            _ => {
                // An unrecognised command: skip its numeric operand if there is one, so the
                // scan cannot desynchronise and read a coordinate as a command letter.
                if let Some((_, next)) = number(d, i) {
                    i = next;
                }
            }
        }
    }
    bounds
}

/// Widens `bounds` to cover `(x, y)`, truncating each coordinate toward the low corner.
///
/// # Why `floor` on both ends, and not `floor`/`ceil`
///
/// `floor`/`ceil` is what a *geometric* bound would do, but it moves the box's **midpoint**: a
/// glyph whose ink runs `20.0..29.0` would bound as `20..=30`, shifting the centre by half a pixel,
/// and the tests that assert "this ink is centred on that line" compare midpoints and fail by one.
/// Truncating both ends keeps the convention this function has always had — for an integer path
/// `floor` is the identity, so every 1-bit face's box is unchanged bit for bit — while still
/// reading a fractional coordinate instead of refusing it.
///
/// The consequence is that a box's high corner can be up to one pixel short of the true ink, which
/// is the same convention as before this function learned about fractions. Callers use the box to
/// assert *placement* (is the ink inside the field, is it centred), not to measure the ink's exact
/// extent, so a conservative high corner is the right trade.
fn include(bounds: &mut Option<(i32, i32, i32, i32)>, x: f32, y: f32) {
    let (x, y) = (x.floor() as i32, y.floor() as i32);
    *bounds = Some(match *bounds {
        None => (x, y, x, y),
        Some((left, top, right, bottom)) => (left.min(x), top.min(y), right.max(x), bottom.max(y)),
    });
}

/// Reads a run of digits (and an optional leading `-`) starting at `at`, as a float.
///
/// # Why a float and not an integer
///
/// The 1-bit face's subpaths are whole pixels (`M8 4h3v4h-3z`), but a **glyph outline** carries
/// fractional vertices (`M21.45 30.24`), because sub-pixel precision is the whole point of drawing
/// an outline instead of a bitmap. An integer reader returns `None` at the `.`, which makes
/// [`path_bounds`] silently answer "no ink" for every vector-rendered glyph.
fn number(text: &str, at: usize) -> Option<(f32, usize)> {
    let bytes = text.as_bytes();
    let mut i = at;
    while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b',') {
        i += 1;
    }
    let start = i;
    if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
        i += 1;
    }
    let digits_start = i;
    while i < bytes.len() && bytes[i].is_ascii_digit() {
        i += 1;
    }
    if i < bytes.len() && bytes[i] == b'.' {
        i += 1;
        while i < bytes.len() && bytes[i].is_ascii_digit() {
            i += 1;
        }
    }
    if i == digits_start {
        return None;
    }
    text[start..i].parse().ok().map(|value: f32| (value, i))
}

/// Reads two numbers separated by whitespace or a comma.
fn number_pair(text: &str, at: usize) -> Option<(f32, f32, usize)> {
    let (first, after_first) = number(text, at)?;
    let (second, after_second) = number(text, after_first)?;
    Some((first, second, after_second))
}

/// The value of a string SVG attribute, e.g. `d` in `<path d="..." />`.
fn attribute_str<'a>(line: &'a str, name: &str) -> Option<&'a str> {
    let key = format!("{name}=\"");
    let at = line.find(&key)? + key.len();
    let end = line[at..].find('"')? + at;
    Some(&line[at..end])
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::compat::MiniToString;
    use crate::core::Font;
    use crate::core::Rect;
    use crate::widget::Button;

    #[test]
    fn render_widget_to_svg_produces_valid_svg() {
        let mut btn = Button::new("OK".to_string(), Rect::new(0, 0, 80, 30));
        let svg = render_widget_to_svg(&mut btn, Rect::new(0, 0, 80, 30));
        assert!(svg.starts_with("<svg"));
        assert!(svg.ends_with("</svg>"));
        assert!(svg.contains("width=\"80\""));
        assert!(svg.contains("height=\"30\""));
    }

    #[test]
    fn render_widget_to_svg_contains_elements() {
        let mut btn = Button::new("Click Me".to_string(), Rect::new(0, 0, 120, 40));
        let svg = render_widget_to_svg(&mut btn, Rect::new(0, 0, 120, 40));
        // Should contain at least one fill/stroke/rect element
        assert!(svg.contains("fill=") || svg.contains("stroke="));
    }

    #[test]
    fn render_to_svg_wrapper_works() {
        let mut btn = Button::new("OK".to_string(), Rect::new(0, 0, 80, 30));
        let svg = render_to_svg(&mut btn);
        assert!(svg.starts_with("<svg"));
        assert!(svg.contains("width=\"80\""));
    }

    /// The emitted SVG ink box equals the rasteriser's ink box, exactly.
    ///
    /// This is the guarantee that makes the snapshots a trustworthy picture of the control: the
    /// software rasteriser blits an 8x8 bitmap across the **whole** glyph box (`0..height`
    /// measured down from `origin.y`), and the SVG backend emits those same rectangles as
    /// `<path>` subpaths. Both read `glyph_rects`, so the test asserts the property the shared
    /// derivation exists to provide — and it would be silent to break, because a label would
    /// simply be a few pixels off in every snapshot, which no other gate can see.
    ///
    /// The old form of this test compared an emitted baseline against `origin.y + ascent`,
    /// which was a check on the *legacy* `<text>` path. There is no baseline any more, and the
    /// replacement is stronger: the top-left of the ink is asserted to be the glyph box's
    /// top-left, and the box is asserted to be `line_height` tall with `"Sample"`'s own width.
    #[test]
    fn the_emitted_path_reproduces_the_glyph_box_exactly() {
        // Holds the crate-wide theme guard: this test renders, and a concurrent
        // test that switches the appearance would otherwise change a later frame.
        let _theme_guard = crate::style::theme_test_guard();
        use crate::render::text::estimate_cluster_advance;
        for size in [11.0f32, 12.0, 13.0, 14.0, 20.0, 48.0] {
            let font = Font::new("Arial", size, false, false);
            let origin = crate::core::Point::new(10, 53);
            let mut backend = crate::render::SvgPaintBackend::new(Size::new(400, 160));
            {
                use crate::render::RenderContext;
                let mut context = RenderContext::new(&mut backend);
                context.draw_text(
                    origin,
                    "Sample",
                    &font,
                    crate::core::Color::BLACK,
                    crate::core::HorizontalAlignment::Left,
                );
            }
            let document = backend.finish();

            let (left, top, right, bottom) =
                text_ink_box(&document).expect("a text path was emitted");

            // The glyph box's top edge is where the rasteriser put it. The bottom edge is one
            // line height down, because the bitmap is stretched across the whole box.
            let height = font.size().max(1.0).round() as i32;
            assert_eq!(top, origin.y, "size {size}: the glyph box top edge");
            assert_eq!(bottom, origin.y + height, "size {size}: the glyph box bottom edge");
            // And the ink starts at the left edge, because `Left` alignment anchors there and
            // `S`'s bitmap has its leftmost set bit in column 0.
            assert_eq!(left, origin.x, "size {size}: left-aligned ink starts at the origin");
            // The ink cannot be wider than the string's own advance. `estimate_cluster_advance`
            // charges one cluster at a time, so the run's advance is the sum over `"Sample"`'s
            // six clusters — the same sum `shape_text` performs.
            let advance: i32 = "Sample"
                .chars()
                .map(|ch| estimate_cluster_advance(&ch.to_string(), size, 1.0).round() as i32)
                .sum();
            assert!(
                right - left <= advance,
                "size {size}: ink width {} exceeds the {advance}px advance",
                right - left
            );
            assert!(right > left, "size {size}: the string drew no ink at all");
        }
    }

    /// Every rendered glyph contributes subpaths, so `text_subpath_count` separates "drew a
    /// string" from "drew nothing" — the assertion the deleted `svg.contains("OK")` form used
    /// to make, before text stopped being a `<text>` element.
    #[test]
    fn text_subpath_count_separates_a_drawn_string_from_an_empty_one() {
        // Holds the crate-wide theme guard: this test renders, and a concurrent
        // test that switches the appearance would otherwise change a later frame.
        let _theme_guard = crate::style::theme_test_guard();
        let font = Font::new("Arial", 14.0, false, false);
        let paint = |text: &str| {
            let mut backend = crate::render::SvgPaintBackend::new(Size::new(200, 60));
            {
                use crate::render::RenderContext;
                let mut context = RenderContext::new(&mut backend);
                context.draw_text(
                    crate::core::Point::new(4, 4),
                    text,
                    &font,
                    crate::core::Color::BLACK,
                    crate::core::HorizontalAlignment::Left,
                );
            }
            backend.finish()
        };
        assert_eq!(text_subpath_count(&paint("")), 0);
        assert_eq!(text_subpath_count(&paint(" ")), 0);
        let one = text_subpath_count(&paint("O"));
        let two = text_subpath_count(&paint("OO"));
        assert!(one > 0, "a drawn glyph has set bits");
        // Two identical glyphs a pen apart double the ink. This is the property a lost
        // per-cluster advance would break, and it is invisible to a bounding-box assertion.
        assert_eq!(two, one * 2, "the pen advanced so the second O is a second glyph");
    }

    /// The readers refuse a document they cannot understand, rather than inventing an answer.
    #[test]
    fn reading_text_geometry_from_a_document_without_text_is_none() {
        assert_eq!(text_ink_box(r#"<rect x="0" y="5" width="1" height="1" />"#), None);
        assert_eq!(text_ink_box(r##"<path d="" fill="#000" />"##), None);
        assert_eq!(text_ink_box(r##"<path fill="#000" />"##), None);
        assert_eq!(text_subpath_count(r#"<rect x="0" y="1" width="2" height="3" />"#), 0);
        // One subpath: `M0 0h1v1h-1z` is a unit square at the top-left of the box.
        assert_eq!(text_ink_box("<path d=\"M0 0h1v1h-1z\" fill=\"#000\" />"), Some((0, 0, 1, 1)));
    }
}