Skip to main content

ferroui_base/rendering/composition/server/dirty_rects/
i_dirty_rect_tracker.rs

1use crate::platform::{IDrawingContextImpl, LtrbPixelRect, LtrbRect};
2use std::cell::Cell;
3use std::hash::{BuildHasher, Hasher};
4
5/// Accumulates the dirty area of a frame and clips drawing to it.
6///
7/// Trackers are render-thread objects: every member takes `&self` and the
8/// implementations use interior mutability.
9pub trait IDirtyRectTracker: IDirtyRectCollector {
10    /// Post-processes the dirty rect area (e. g. to account for anti-aliasing)
11    fn finalize_frame(&self, bounds: LtrbRect);
12
13    /// Pushes the dirty area as a clip onto `ctx`.
14    ///
15    /// The C# member returns a disposable that pops the clip. Drawing
16    /// contexts are passed around as `&mut dyn IDrawingContextImpl` here, so
17    /// the pair is explicit instead: every `begin_draw` must be followed by
18    /// exactly one [`end_draw`](Self::end_draw) on the same context once
19    /// drawing is done.
20    fn begin_draw(&self, ctx: &mut dyn IDrawingContextImpl);
21
22    /// Pops the clip pushed by [`begin_draw`](Self::begin_draw).
23    fn end_draw(&self, ctx: &mut dyn IDrawingContextImpl);
24
25    fn is_empty(&self) -> bool;
26
27    fn intersects(&self, rect: LtrbRect) -> bool;
28
29    fn initialize(&self, bounds: LtrbRect);
30
31    /// Draws the dirty area in a random color (debug overlay).
32    fn visualize(&self, context: &mut dyn IDrawingContextImpl);
33
34    fn combined_rect(&self) -> LtrbRect;
35
36    /// Whether the tracker is a `SingleDirtyRectTracker` (the `is` test of
37    /// the render pass, which needs no per-visual test with one).
38    fn is_single_dirty_rect_tracker(&self) -> bool {
39        false
40    }
41}
42
43/// Receives invalidated rectangles.
44pub trait IDirtyRectCollector {
45    fn add_rect(&self, rect: LtrbRect);
46}
47
48// --- helpers shared by the trackers -----------------------------------------
49//
50// The following are members of `LtrbRect` / `LtrbPixelRect` in the C# source
51// that the `platform` module does not have yet
52// (see `patches/0001-platform-ltrb-rect-members.patch`).
53
54/// C# `LtrbRect.FullUnion(LtrbRect?, LtrbRect?)`.
55pub(super) fn full_union(left: Option<LtrbRect>, right: Option<LtrbRect>) -> Option<LtrbRect> {
56    match (left, right) {
57        (None, right) => right,
58        (left, None) => left,
59        (Some(left), Some(right)) => Some(right.union(left)),
60    }
61}
62
63/// C# `LtrbRect.Contains(LtrbRect)`.
64pub(super) fn contains_rect(outer: &LtrbRect, rect: LtrbRect) -> bool {
65    rect.left >= outer.left && rect.right <= outer.right && rect.top >= outer.top && rect.bottom <= outer.bottom
66}
67
68/// C# `LtrbPixelRect.FromRectUnscaled`: left/top are truncated towards zero,
69/// right/bottom are rounded up.
70pub(super) fn pixel_rect_from_rect_unscaled(rect: LtrbRect) -> LtrbPixelRect {
71    LtrbPixelRect::new(rect.left as i32, rect.top as i32, rect.right.ceil() as i32, rect.bottom.ceil() as i32)
72}
73
74/// C# `LtrbPixelRect.ToLtrbRectUnscaled`.
75pub(super) fn pixel_rect_to_ltrb_rect_unscaled(rect: LtrbPixelRect) -> LtrbRect {
76    LtrbRect::new(rect.left as f64, rect.top as f64, rect.right as f64, rect.bottom as f64)
77}
78
79/// The pseudo random number source of the `visualize` overlays (C#
80/// `System.Random`): the colors only have to differ from frame to frame.
81pub(super) struct VisualizeRandom {
82    state: Cell<u64>,
83}
84
85impl VisualizeRandom {
86    pub(super) fn new() -> Self {
87        // Seeded from the per-process hasher keys; no clock involved.
88        let seed = std::collections::hash_map::RandomState::new().build_hasher().finish();
89        Self { state: Cell::new(seed | 1) }
90    }
91
92    /// A value in `0..max_value` (C# `Random.Next(maxValue)`).
93    pub(super) fn next(&self, max_value: i32) -> i32 {
94        // xorshift64*
95        let mut x = self.state.get();
96        x ^= x >> 12;
97        x ^= x << 25;
98        x ^= x >> 27;
99        self.state.set(x);
100        let value = x.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 33;
101        if max_value <= 0 {
102            0
103        } else {
104            (value % max_value as u64) as i32
105        }
106    }
107}
108
109#[cfg(test)]
110pub(crate) mod tests {
111    use super::*;
112    use crate::media::{BoxShadows, Color, IBrush, IPen, RenderOptions};
113    use crate::platform::{
114        IBitmapImpl, IDrawingContextLayerImpl, IGeometryImpl, IGlyphRunImpl, IPlatformRenderInterfaceRegion,
115    };
116    use crate::{Matrix, PixelSize, Point, Rect, RoundedRect};
117    use std::any::{Any, TypeId};
118    use std::cell::RefCell;
119    use std::rc::Rc;
120    use std::sync::Arc;
121
122    /// A drawing context that records the calls it receives.
123    pub(crate) struct MockDrawingContext {
124        pub log: Vec<String>,
125        pub transform: Matrix,
126    }
127
128    impl Default for MockDrawingContext {
129        fn default() -> Self {
130            Self { log: Vec::new(), transform: Matrix::IDENTITY }
131        }
132    }
133
134    fn describe_brush(brush: Option<&dyn IBrush>) -> String {
135        match brush {
136            None => "none".to_string(),
137            Some(brush) => match brush.as_solid_color_brush() {
138                Some(solid) => format!("#{:08x}@{}", solid.color().to_uint32(), brush.opacity()),
139                None => "brush".to_string(),
140            },
141        }
142    }
143
144    impl IDrawingContextImpl for MockDrawingContext {
145        fn transform(&self) -> Matrix {
146            self.transform
147        }
148        fn set_transform(&mut self, value: Matrix) {
149            self.transform = value;
150        }
151        fn clear(&mut self, _color: Color) {
152            self.log.push("clear".to_string());
153        }
154        fn draw_bitmap(&mut self, _: &dyn IBitmapImpl, _: f64, _: Rect, _: Rect) {
155            self.log.push("bitmap".to_string());
156        }
157        fn draw_bitmap_with_mask(&mut self, _: &dyn IBitmapImpl, _: &dyn IBrush, _: Rect, _: Rect) {
158            self.log.push("bitmap_mask".to_string());
159        }
160        fn draw_line(&mut self, _: Option<&dyn IPen>, p1: Point, p2: Point) {
161            self.log.push(format!("line {p1} {p2}"));
162        }
163        fn draw_geometry(&mut self, brush: Option<&dyn IBrush>, pen: Option<&dyn IPen>, _: &dyn IGeometryImpl) {
164            let pen = pen.map(|p| describe_brush(p.brush().as_deref())).unwrap_or("none".to_string());
165            self.log.push(format!(
166                "geometry {} pen {pen} at {},{}",
167                describe_brush(brush),
168                self.transform.m31,
169                self.transform.m32
170            ));
171        }
172        fn draw_rectangle(
173            &mut self,
174            brush: Option<&dyn IBrush>,
175            _pen: Option<&dyn IPen>,
176            rect: RoundedRect,
177            _box_shadows: &BoxShadows,
178        ) {
179            self.log.push(format!("rect {} {}", describe_brush(brush), rect.rect));
180        }
181        fn draw_region(
182            &mut self,
183            brush: Option<&dyn IBrush>,
184            _pen: Option<&dyn IPen>,
185            region: &dyn IPlatformRenderInterfaceRegion,
186        ) {
187            let alpha = brush.and_then(|b| b.as_solid_color_brush()).map(|b| b.color().a).unwrap_or(0);
188            self.log.push(format!("region alpha {alpha} rects {}", region.rects().len()));
189        }
190        fn draw_ellipse(&mut self, _: Option<&dyn IBrush>, _: Option<&dyn IPen>, _: Rect) {
191            self.log.push("ellipse".to_string());
192        }
193        fn draw_glyph_run(&mut self, foreground: Option<&dyn IBrush>, glyph_run: &dyn IGlyphRunImpl) {
194            use crate::rendering::composition::server::diagnostic_text_renderer::tests::MockTextGlyphRunImpl;
195
196            // A run of shaped diagnostic text is logged with its text.
197            if let Some(text) = glyph_run.as_any().downcast_ref::<MockTextGlyphRunImpl>() {
198                self.log.push(format!(
199                    "text {} {} at {},{}",
200                    text.0,
201                    describe_brush(foreground),
202                    self.transform.m31,
203                    self.transform.m32
204                ));
205                return;
206            }
207
208            self.log.push(format!(
209                "glyph {} {} at {},{}",
210                glyph_run.font_rendering_em_size(),
211                describe_brush(foreground),
212                self.transform.m31,
213                self.transform.m32
214            ));
215        }
216        fn create_layer(&mut self, _: PixelSize) -> Rc<dyn IDrawingContextLayerImpl> {
217            unimplemented!()
218        }
219        fn push_clip(&mut self, clip: Rect) {
220            self.log.push(format!("push_clip {clip}"));
221        }
222        fn push_clip_rounded(&mut self, clip: RoundedRect) {
223            self.log.push(format!("push_clip_rounded {}", clip.rect));
224        }
225        fn push_clip_region(&mut self, region: &dyn IPlatformRenderInterfaceRegion) {
226            self.log.push(format!("push_clip_region {:?}", region.rects()));
227        }
228        fn pop_clip(&mut self) {
229            self.log.push("pop_clip".to_string());
230        }
231        fn push_layer(&mut self, _: Rect) {}
232        fn pop_layer(&mut self) {}
233        fn push_opacity(&mut self, _: f64, _: Option<Rect>) {}
234        fn pop_opacity(&mut self) {}
235        fn push_opacity_mask(&mut self, _: &dyn IBrush, _: Rect) {}
236        fn pop_opacity_mask(&mut self) {}
237        fn push_geometry_clip(&mut self, _: &dyn IGeometryImpl) {}
238        fn pop_geometry_clip(&mut self) {}
239        fn push_render_options(&mut self, _: RenderOptions) {}
240        fn pop_render_options(&mut self) {}
241        fn push_text_options(&mut self, _: crate::media::TextOptions) {}
242        fn pop_text_options(&mut self) {}
243        fn get_feature(&mut self, _: TypeId) -> Option<Rc<dyn Any>> {
244            None
245        }
246        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
247            self
248        }
249        fn dispose(&mut self) {}
250    }
251
252    /// A region that keeps the list of rectangles added to it.
253    #[derive(Default)]
254    pub(crate) struct MockRegion {
255        pub rects: RefCell<Vec<LtrbPixelRect>>,
256    }
257
258    impl IPlatformRenderInterfaceRegion for MockRegion {
259        fn add_rect(&self, rect: LtrbPixelRect) {
260            self.rects.borrow_mut().push(rect);
261        }
262        fn reset(&self) {
263            self.rects.borrow_mut().clear();
264        }
265        fn is_empty(&self) -> bool {
266            self.rects.borrow().is_empty()
267        }
268        fn bounds(&self) -> LtrbPixelRect {
269            let rects = self.rects.borrow();
270            let mut iter = rects.iter();
271            match iter.next() {
272                None => LtrbPixelRect::default(),
273                Some(first) => iter.fold(*first, |acc, r| acc.union(*r)),
274            }
275        }
276        fn rects(&self) -> Vec<LtrbPixelRect> {
277            self.rects.borrow().clone()
278        }
279        fn intersects(&self, rect: LtrbRect) -> bool {
280            self.rects.borrow().iter().any(|r| pixel_rect_to_ltrb_rect_unscaled(*r).intersects(rect))
281        }
282        fn contains(&self, pt: Point) -> bool {
283            self.rects.borrow().iter().any(|r| pixel_rect_to_ltrb_rect_unscaled(*r).contains(pt.x, pt.y))
284        }
285        fn dispose(&self) {}
286        fn as_any(&self) -> &dyn Any {
287            self
288        }
289    }
290
291    /// A stream geometry built by a [`MockStreamGeometryBuilder`], with the
292    /// log of the figure commands it was built from.
293    pub(crate) struct MockStreamGeometry {
294        pub log: Arc<std::sync::Mutex<Vec<String>>>,
295    }
296
297    /// A stream geometry builder that records the figure commands it
298    /// receives.
299    struct MockStreamGeometryBuilder {
300        log: Arc<std::sync::Mutex<Vec<String>>>,
301    }
302
303    impl crate::platform::IGeometryContext for MockStreamGeometryBuilder {
304        fn arc_to(&mut self, _: Point, _: crate::Size, _: f64, _: bool, _: crate::media::SweepDirection, _: bool) {
305            self.log.lock().unwrap().push("arc".to_string());
306        }
307        fn begin_figure(&mut self, start_point: Point, is_filled: bool) {
308            self.log.lock().unwrap().push(format!("begin {start_point} {is_filled}"));
309        }
310        fn cubic_bezier_to(&mut self, _: Point, _: Point, _: Point, _: bool) {
311            self.log.lock().unwrap().push("cubic".to_string());
312        }
313        fn quadratic_bezier_to(&mut self, _: Point, _: Point, _: bool) {
314            self.log.lock().unwrap().push("quad".to_string());
315        }
316        fn line_to(&mut self, point: Point, is_stroked: bool) {
317            self.log.lock().unwrap().push(format!("line {point} {is_stroked}"));
318        }
319        fn end_figure(&mut self, is_closed: bool) {
320            self.log.lock().unwrap().push(format!("end {is_closed}"));
321        }
322        fn set_fill_rule(&mut self, _: crate::media::FillRule) {
323            self.log.lock().unwrap().push("fill".to_string());
324        }
325        fn dispose(&mut self) {
326            self.log.lock().unwrap().push("dispose".to_string());
327        }
328    }
329
330    impl crate::platform::IStreamGeometryBuilder for MockStreamGeometryBuilder {
331        fn to_geometry(&mut self) -> Arc<dyn IGeometryImpl> {
332            Arc::new(MockStreamGeometry { log: self.log.clone() })
333        }
334    }
335
336    impl IGeometryImpl for MockStreamGeometry {
337        fn as_any(&self) -> &dyn std::any::Any {
338            self
339        }
340        fn bounds(&self) -> Rect {
341            Rect::default()
342        }
343        fn contour_length(&self) -> f64 {
344            0.0
345        }
346        fn get_render_bounds(&self, _pen: Option<&dyn IPen>) -> Rect {
347            Rect::default()
348        }
349        fn get_widened_geometry(&self, _pen: &dyn IPen) -> Arc<dyn IGeometryImpl> {
350            unimplemented!()
351        }
352        fn fill_contains(&self, _point: Point) -> bool {
353            false
354        }
355        fn get_fill_intersection_result(&self, _geometry: &dyn IGeometryImpl) -> crate::media::IntersectionResult {
356            crate::media::IntersectionResult::Empty
357        }
358        fn intersect(&self, _geometry: &dyn IGeometryImpl) -> Option<Arc<dyn IGeometryImpl>> {
359            None
360        }
361        fn stroke_contains(&self, _pen: Option<&dyn IPen>, _point: Point) -> bool {
362            false
363        }
364        fn try_get_point_at_distance(&self, _distance: f64) -> Option<Point> {
365            None
366        }
367        fn try_get_point_and_tangent_at_distance(&self, _distance: f64) -> Option<(Point, Point)> {
368            None
369        }
370        fn try_get_segment(&self, _start: f64, _stop: f64, _begin: bool) -> Option<Arc<dyn IGeometryImpl>> {
371            None
372        }
373        fn with_transform(&self, _: Matrix) -> Arc<dyn crate::platform::ITransformedGeometryImpl> {
374            unimplemented!()
375        }
376    }
377
378    /// A render interface that creates [`MockRegion`]s and
379    /// [`MockStreamGeometry`]s and keeps track of them.
380    #[derive(Default)]
381    pub(crate) struct MockRenderInterface {
382        pub regions: RefCell<Vec<Rc<MockRegion>>>,
383        pub streams: RefCell<Vec<Arc<std::sync::Mutex<Vec<String>>>>>,
384    }
385
386    impl crate::platform::IPlatformRenderInterface for MockRenderInterface {
387        fn build_glyph_run_geometry(&self, _: &crate::media::GlyphRun) -> Arc<dyn IGeometryImpl> {
388            unimplemented!()
389        }
390        fn create_glyph_run(
391            &self,
392            _: &Rc<crate::media::GlyphTypeface>,
393            _: f64,
394            _: &[crate::media::text_formatting::GlyphInfo],
395            _: Point,
396        ) -> std::sync::Arc<dyn crate::platform::IGlyphRunImpl> {
397            unimplemented!()
398        }
399        fn create_ellipse_geometry(&self, _: Rect) -> Arc<dyn IGeometryImpl> {
400            unimplemented!()
401        }
402        fn create_line_geometry(&self, _: Point, _: Point) -> Arc<dyn IGeometryImpl> {
403            unimplemented!()
404        }
405        fn create_rectangle_geometry(&self, _: Rect) -> Arc<dyn IGeometryImpl> {
406            unimplemented!()
407        }
408        fn create_stream_geometry_builder(
409            &self,
410            _source: Option<&dyn IGeometryImpl>,
411        ) -> Box<dyn crate::platform::IStreamGeometryBuilder> {
412            let log = Arc::new(std::sync::Mutex::new(Vec::new()));
413            self.streams.borrow_mut().push(log.clone());
414            Box::new(MockStreamGeometryBuilder { log })
415        }
416        fn create_geometry_group(&self, _: crate::media::FillRule, _: &[Arc<dyn IGeometryImpl>]) -> Arc<dyn IGeometryImpl> {
417            unimplemented!()
418        }
419        fn create_combined_geometry(
420            &self,
421            _: crate::media::GeometryCombineMode,
422            _: Arc<dyn IGeometryImpl>,
423            _: Arc<dyn IGeometryImpl>,
424        ) -> Arc<dyn IGeometryImpl> {
425            unimplemented!()
426        }
427        fn create_render_target_bitmap(
428            &self,
429            _: crate::PixelSize,
430            _: crate::Vector,
431        ) -> std::sync::Arc<dyn crate::platform::IRenderTargetBitmapImpl> {
432            unimplemented!()
433        }
434        fn create_writeable_bitmap(
435            &self,
436            _: crate::PixelSize,
437            _: crate::Vector,
438            _: crate::platform::PixelFormat,
439            _: crate::platform::AlphaFormat,
440        ) -> std::sync::Arc<dyn crate::platform::IWriteableBitmapImpl> {
441            unimplemented!()
442        }
443        fn load_bitmap_from_file(&self, _: &str) -> std::io::Result<std::sync::Arc<crate::platform::SharedBitmapImpl>> {
444            unimplemented!()
445        }
446        fn load_bitmap(&self, _: &mut dyn std::io::Read) -> std::io::Result<std::sync::Arc<crate::platform::SharedBitmapImpl>> {
447            unimplemented!()
448        }
449        fn load_writeable_bitmap_to_width(
450            &self,
451            _: &mut dyn std::io::Read,
452            _: i32,
453            _: crate::media::imaging::BitmapInterpolationMode,
454        ) -> std::io::Result<std::sync::Arc<dyn crate::platform::IWriteableBitmapImpl>> {
455            unimplemented!()
456        }
457        fn load_writeable_bitmap_to_height(
458            &self,
459            _: &mut dyn std::io::Read,
460            _: i32,
461            _: crate::media::imaging::BitmapInterpolationMode,
462        ) -> std::io::Result<std::sync::Arc<dyn crate::platform::IWriteableBitmapImpl>> {
463            unimplemented!()
464        }
465        fn load_writeable_bitmap_from_file(
466            &self,
467            _: &str,
468        ) -> std::io::Result<std::sync::Arc<dyn crate::platform::IWriteableBitmapImpl>> {
469            unimplemented!()
470        }
471        fn load_writeable_bitmap(
472            &self,
473            _: &mut dyn std::io::Read,
474        ) -> std::io::Result<std::sync::Arc<dyn crate::platform::IWriteableBitmapImpl>> {
475            unimplemented!()
476        }
477        fn load_bitmap_to_width(
478            &self,
479            _: &mut dyn std::io::Read,
480            _: i32,
481            _: crate::media::imaging::BitmapInterpolationMode,
482        ) -> std::io::Result<std::sync::Arc<crate::platform::SharedBitmapImpl>> {
483            unimplemented!()
484        }
485        fn load_bitmap_to_height(
486            &self,
487            _: &mut dyn std::io::Read,
488            _: i32,
489            _: crate::media::imaging::BitmapInterpolationMode,
490        ) -> std::io::Result<std::sync::Arc<crate::platform::SharedBitmapImpl>> {
491            unimplemented!()
492        }
493        fn resize_bitmap(
494            &self,
495            _: &dyn crate::platform::IBitmapImpl,
496            _: crate::PixelSize,
497            _: crate::media::imaging::BitmapInterpolationMode,
498        ) -> std::sync::Arc<crate::platform::SharedBitmapImpl> {
499            unimplemented!()
500        }
501        fn load_bitmap_from_pixels(
502            &self,
503            _: crate::platform::PixelFormat,
504            _: crate::platform::AlphaFormat,
505            _: &[u8],
506            _: crate::PixelSize,
507            _: crate::Vector,
508            _: i32,
509        ) -> std::sync::Arc<crate::platform::SharedBitmapImpl> {
510            unimplemented!()
511        }
512        fn create_backend_context(
513            &self,
514            _: Option<Rc<dyn crate::platform::IPlatformGraphicsContext>>,
515        ) -> Rc<dyn crate::platform::IPlatformRenderInterfaceContext> {
516            unimplemented!()
517        }
518        fn supports_individual_round_rects(&self) -> bool {
519            false
520        }
521        fn default_alpha_format(&self) -> crate::platform::AlphaFormat {
522            crate::platform::AlphaFormat::Premul
523        }
524        fn default_pixel_format(&self) -> crate::platform::PixelFormat {
525            crate::platform::PixelFormats::RGBA8888
526        }
527        fn is_supported_bitmap_pixel_format(&self, _: crate::platform::PixelFormat) -> bool {
528            true
529        }
530        fn supports_regions(&self) -> bool {
531            true
532        }
533        fn create_region(&self) -> Rc<dyn IPlatformRenderInterfaceRegion> {
534            let region = Rc::new(MockRegion::default());
535            self.regions.borrow_mut().push(region.clone());
536            region
537        }
538    }
539
540    #[test]
541    fn full_union_treats_none_as_identity() {
542        let a = LtrbRect::new(0.0, 0.0, 10.0, 10.0);
543        let b = LtrbRect::new(5.0, 5.0, 20.0, 30.0);
544        assert_eq!(None, full_union(None, None));
545        assert_eq!(Some(a), full_union(Some(a), None));
546        assert_eq!(Some(b), full_union(None, Some(b)));
547        assert_eq!(Some(LtrbRect::new(0.0, 0.0, 20.0, 30.0)), full_union(Some(a), Some(b)));
548    }
549
550    #[test]
551    fn pixel_rect_conversion_truncates_left_top_and_ceils_right_bottom() {
552        let px = pixel_rect_from_rect_unscaled(LtrbRect::new(1.7, 2.2, 3.1, 4.0));
553        assert_eq!(LtrbPixelRect::new(1, 2, 4, 4), px);
554        // Truncation is towards zero, not a floor.
555        let px = pixel_rect_from_rect_unscaled(LtrbRect::new(-1.7, -0.2, -0.5, 0.5));
556        assert_eq!(LtrbPixelRect::new(-1, 0, 0, 1), px);
557        assert_eq!(LtrbRect::new(-1.0, 0.0, 0.0, 1.0), pixel_rect_to_ltrb_rect_unscaled(px));
558    }
559
560    #[test]
561    fn contains_rect_is_inclusive() {
562        let outer = LtrbRect::new(0.0, 0.0, 10.0, 10.0);
563        assert!(contains_rect(&outer, outer));
564        assert!(contains_rect(&outer, LtrbRect::new(1.0, 1.0, 9.0, 9.0)));
565        assert!(!contains_rect(&outer, LtrbRect::new(1.0, 1.0, 10.5, 9.0)));
566    }
567
568    #[test]
569    fn visualize_random_stays_in_range() {
570        let random = VisualizeRandom::new();
571        for _ in 0..1000 {
572            let value = random.next(255);
573            assert!((0..255).contains(&value));
574        }
575    }
576}