1use crate::item_rendering::{
19 ItemRenderer, ItemRendererFeatures, RenderBorderRectangle, RenderImage, RenderRectangle,
20 RenderText,
21};
22use crate::item_tree::{ItemTreeRc, ItemTreeWeak, ItemVisitorResult};
23#[cfg(feature = "std")]
24use crate::items::Path;
25use crate::items::{BoxShadow, Clip, ItemRc, ItemRef, Opacity, RenderingResult, TextInput};
26use crate::lengths::{
27 ItemTransform, LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalPx, LogicalRect,
28 LogicalSize, LogicalVector,
29};
30use crate::properties::PropertyTracker;
31use crate::window::WindowAdapter;
32use crate::Coord;
33use alloc::boxed::Box;
34use alloc::rc::Rc;
35use core::cell::{Cell, RefCell};
36use core::pin::Pin;
37
38#[derive(Default, Debug)]
41#[repr(C)]
42pub struct CachedRenderingData {
43 pub(crate) cache_index: Cell<usize>,
45 pub(crate) cache_generation: Cell<usize>,
49}
50
51impl CachedRenderingData {
52 fn release(
56 &self,
57 cache: &mut PartialRendererCache,
58 ) -> Option<CachedItemBoundingBoxAndTransform> {
59 if self.cache_generation.get() == cache.generation() {
60 let index = self.cache_index.get();
61 self.cache_generation.set(0);
62 Some(cache.remove(index).data)
63 } else {
64 None
65 }
66 }
67
68 fn get_entry<'a>(
70 &self,
71 cache: &'a mut PartialRendererCache,
72 ) -> Option<&'a mut PartialRenderingCachedData> {
73 let index = self.cache_index.get();
74 if self.cache_generation.get() == cache.generation() {
75 cache.get_mut(index)
76 } else {
77 None
78 }
79 }
80}
81
82#[derive(Clone, PartialEq)]
85pub enum CachedItemBoundingBoxAndTransform {
86 RegularItem {
88 bounding_rect: LogicalRect,
90 offset: LogicalVector,
92 },
93 ItemWithTransform {
95 bounding_rect: LogicalRect,
97 transform: Box<ItemTransform>,
99 },
100 ClipItem {
102 geometry: LogicalRect,
104 },
105}
106
107impl CachedItemBoundingBoxAndTransform {
108 fn bounding_rect(&self) -> &LogicalRect {
109 match self {
110 CachedItemBoundingBoxAndTransform::RegularItem { bounding_rect, .. } => bounding_rect,
111 CachedItemBoundingBoxAndTransform::ItemWithTransform { bounding_rect, .. } => {
112 bounding_rect
113 }
114 CachedItemBoundingBoxAndTransform::ClipItem { geometry } => geometry,
115 }
116 }
117
118 fn transform(&self) -> ItemTransform {
119 match self {
120 CachedItemBoundingBoxAndTransform::RegularItem { offset, .. } => {
121 ItemTransform::translation(offset.x as f32, offset.y as f32)
122 }
123 CachedItemBoundingBoxAndTransform::ItemWithTransform { transform, .. } => **transform,
124 CachedItemBoundingBoxAndTransform::ClipItem { geometry } => {
125 ItemTransform::translation(geometry.origin.x as f32, geometry.origin.y as f32)
126 }
127 }
128 }
129
130 fn new<T: ItemRendererFeatures>(
131 item_rc: &ItemRc,
132 window_adapter: &Rc<dyn WindowAdapter>,
133 ) -> Self {
134 let geometry = item_rc.geometry();
135
136 if item_rc.borrow().as_ref().clips_children() {
137 return Self::ClipItem { geometry };
138 }
139
140 let bounding_rect = crate::properties::evaluate_no_tracking(|| {
143 item_rc.bounding_rect(&geometry, window_adapter)
144 });
145
146 if let Some(complex_child_transform) = (T::SUPPORTS_TRANSFORMATIONS
147 && window_adapter.renderer().supports_transformations())
148 .then(|| item_rc.children_transform())
149 .flatten()
150 {
151 Self::ItemWithTransform {
152 bounding_rect,
153 transform: complex_child_transform
154 .then_translate(geometry.origin.to_vector().cast())
155 .into(),
156 }
157 } else {
158 Self::RegularItem { bounding_rect, offset: geometry.origin.to_vector() }
159 }
160 }
161}
162
163struct PartialRenderingCachedData {
164 pub data: CachedItemBoundingBoxAndTransform,
166 pub tracker: Option<core::pin::Pin<Box<PropertyTracker>>>,
168}
169impl PartialRenderingCachedData {
170 fn new(data: CachedItemBoundingBoxAndTransform) -> Self {
171 Self { data, tracker: None }
172 }
173}
174
175struct PartialRendererCache {
177 slab: slab::Slab<PartialRenderingCachedData>,
178 generation: usize,
179}
180
181impl Default for PartialRendererCache {
182 fn default() -> Self {
183 Self { slab: Default::default(), generation: 1 }
184 }
185}
186
187impl PartialRendererCache {
188 pub fn generation(&self) -> usize {
191 self.generation
192 }
193
194 pub fn get_mut(&mut self, index: usize) -> Option<&mut PartialRenderingCachedData> {
196 self.slab.get_mut(index)
197 }
198
199 pub fn insert(&mut self, data: PartialRenderingCachedData) -> usize {
201 self.slab.insert(data)
202 }
203
204 pub fn remove(&mut self, index: usize) -> PartialRenderingCachedData {
206 self.slab.remove(index)
207 }
208
209 pub fn clear(&mut self) {
212 self.slab.clear();
213 self.generation += 1;
214 }
215}
216
217#[derive(Default, Clone)]
219pub struct DirtyRegion {
220 rectangles: [euclid::Box2D<Coord, LogicalPx>; Self::MAX_COUNT],
221 count: usize,
222}
223
224impl core::fmt::Debug for DirtyRegion {
225 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
226 write!(f, "{:?}", &self.rectangles[..self.count])
227 }
228}
229
230impl DirtyRegion {
231 pub(crate) const MAX_COUNT: usize = 3;
233
234 pub fn iter(&self) -> impl Iterator<Item = euclid::Box2D<Coord, LogicalPx>> + '_ {
236 (0..self.count).map(|x| self.rectangles[x])
237 }
238
239 pub fn add_rect(&mut self, rect: LogicalRect) {
243 self.add_box(rect.to_box2d());
244 }
245
246 pub fn add_box(&mut self, b: euclid::Box2D<Coord, LogicalPx>) {
250 if b.is_empty() {
251 return;
252 }
253 let mut i = 0;
254 while i < self.count {
255 let r = &self.rectangles[i];
256 if r.contains_box(&b) {
257 return;
259 } else if b.contains_box(r) {
260 self.rectangles.swap(i, self.count - 1);
261 self.count -= 1;
262 continue;
263 }
264 i += 1;
265 }
266
267 if self.count < Self::MAX_COUNT {
268 self.rectangles[self.count] = b;
269 self.count += 1;
270 } else {
271 let best_merge = (0..self.count)
272 .map(|i| (i, self.rectangles[i].union(&b).area() - self.rectangles[i].area()))
273 .min_by(|a, b| PartialOrd::partial_cmp(&a.1, &b.1).unwrap())
274 .expect("There should always be rectangles")
275 .0;
276 self.rectangles[best_merge] = self.rectangles[best_merge].union(&b);
277 }
278 }
279
280 #[must_use]
284 pub fn union(&self, other: &Self) -> Self {
285 let mut s = self.clone();
286 for o in other.iter() {
287 s.add_box(o)
288 }
289 s
290 }
291
292 #[must_use]
294 pub fn bounding_rect(&self) -> LogicalRect {
295 if self.count == 0 {
296 return Default::default();
297 }
298 let mut r = self.rectangles[0];
299 for i in 1..self.count {
300 r = r.union(&self.rectangles[i]);
301 }
302 r.to_rect()
303 }
304
305 #[must_use]
307 pub fn intersection(&self, other: LogicalRect) -> DirtyRegion {
308 let mut ret = self.clone();
309 let other = other.to_box2d();
310 let mut i = 0;
311 while i < ret.count {
312 if let Some(x) = ret.rectangles[i].intersection(&other) {
313 ret.rectangles[i] = x;
314 } else {
315 ret.count -= 1;
316 ret.rectangles.swap(i, ret.count);
317 continue;
318 }
319 i += 1;
320 }
321 ret
322 }
323
324 fn draw_intersects(&self, clipped_geom: LogicalRect) -> bool {
325 let b = clipped_geom.to_box2d();
326 self.iter().any(|r| r.intersects(&b))
327 }
328}
329
330impl From<LogicalRect> for DirtyRegion {
331 fn from(value: LogicalRect) -> Self {
332 let mut s = Self::default();
333 s.add_rect(value);
334 s
335 }
336}
337
338#[derive(PartialEq, Eq, Debug, Clone, Default, Copy)]
341pub enum RepaintBufferType {
342 #[default]
343 NewBuffer,
345 ReusedBuffer,
350
351 SwappedBuffers,
355}
356
357pub struct PartialRenderer<'a, T> {
361 cache: &'a RefCell<PartialRendererCache>,
362 pub dirty_region: DirtyRegion,
364 pub actual_renderer: T,
366 pub window_adapter: Rc<dyn WindowAdapter>,
368}
369
370impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> {
371 fn new(
373 cache: &'a RefCell<PartialRendererCache>,
374 initial_dirty_region: DirtyRegion,
375 actual_renderer: T,
376 ) -> Self {
377 let window_adapter = actual_renderer.window().window_adapter();
378 Self { cache, dirty_region: initial_dirty_region, actual_renderer, window_adapter }
379 }
380
381 pub fn compute_dirty_regions(
383 &mut self,
384 component: &ItemTreeRc,
385 origin: LogicalPoint,
386 size: LogicalSize,
387 ) {
388 #[derive(Clone, Copy)]
389 struct ComputeDirtyRegionState {
390 transform_to_screen: ItemTransform,
391 old_transform_to_screen: ItemTransform,
392 clipped: LogicalRect,
393 must_refresh_children: bool,
394 }
395
396 impl ComputeDirtyRegionState {
397 fn adjust_transforms_for_child(
400 &mut self,
401 children_transform: &ItemTransform,
402 old_children_transform: &ItemTransform,
403 ) {
404 self.transform_to_screen = children_transform.then(&self.transform_to_screen);
405 self.old_transform_to_screen =
406 old_children_transform.then(&self.old_transform_to_screen);
407 }
408 }
409
410 crate::item_tree::visit_items(
411 component,
412 crate::item_tree::TraversalOrder::BackToFront,
413 |component, item, index, state| {
414 let mut new_state = *state;
415 let item_rc = ItemRc::new(component.clone(), index);
416 let rendering_data = item.cached_rendering_data_offset();
417 let mut cache = self.cache.borrow_mut();
418
419 match rendering_data.get_entry(&mut cache) {
420 Some(PartialRenderingCachedData { data: cached_geom, tracker }) => {
421 let rendering_dirty = tracker.as_ref().is_some_and(|tr| tr.is_dirty());
422 let old_geom = cached_geom.clone();
423 let new_geom = CachedItemBoundingBoxAndTransform::new::<T>(
424 &item_rc,
425 &self.window_adapter,
426 );
427
428 let geometry_changed = old_geom != new_geom;
429 if ItemRef::downcast_pin::<Clip>(item).is_some()
430 || ItemRef::downcast_pin::<Opacity>(item).is_some()
431 {
432 new_state.must_refresh_children |= rendering_dirty || geometry_changed;
435
436 if rendering_dirty {
437 *tracker = None;
439 }
440 }
441
442 if geometry_changed {
443 self.mark_dirty_rect(
444 old_geom.bounding_rect(),
445 state.old_transform_to_screen,
446 &state.clipped,
447 );
448 self.mark_dirty_rect(
449 new_geom.bounding_rect(),
450 state.transform_to_screen,
451 &state.clipped,
452 );
453
454 new_state.adjust_transforms_for_child(
455 &new_geom.transform(),
456 &old_geom.transform(),
457 );
458
459 *cached_geom = new_geom;
460
461 return ItemVisitorResult::Continue(new_state);
462 }
463
464 new_state.adjust_transforms_for_child(
465 &cached_geom.transform(),
466 &cached_geom.transform(),
467 );
468
469 if rendering_dirty {
470 self.mark_dirty_rect(
471 cached_geom.bounding_rect(),
472 state.transform_to_screen,
473 &state.clipped,
474 );
475
476 ItemVisitorResult::Continue(new_state)
477 } else {
478 if state.must_refresh_children
479 || new_state.transform_to_screen
480 != new_state.old_transform_to_screen
481 {
482 self.mark_dirty_rect(
483 cached_geom.bounding_rect(),
484 state.old_transform_to_screen,
485 &state.clipped,
486 );
487 self.mark_dirty_rect(
488 cached_geom.bounding_rect(),
489 state.transform_to_screen,
490 &state.clipped,
491 );
492 } else if let Some(tr) = &tracker {
493 tr.as_ref().register_as_dependency_to_current_binding();
494 }
495
496 if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } =
497 &cached_geom
498 {
499 new_state.clipped = new_state
500 .clipped
501 .intersection(
502 &state
503 .transform_to_screen
504 .outer_transformed_rect(&geometry.cast())
505 .cast()
506 .union(
507 &state
508 .old_transform_to_screen
509 .outer_transformed_rect(&geometry.cast())
510 .cast(),
511 ),
512 )
513 .unwrap_or_default();
514 if new_state.clipped.is_empty() {
515 return ItemVisitorResult::SkipChildren;
516 }
517 }
518 ItemVisitorResult::Continue(new_state)
519 }
520 }
521 None => {
522 let geom = CachedItemBoundingBoxAndTransform::new::<T>(
523 &item_rc,
524 &self.window_adapter,
525 );
526 let cache_entry = PartialRenderingCachedData::new(geom.clone());
527 rendering_data.cache_index.set(cache.insert(cache_entry));
528 rendering_data.cache_generation.set(cache.generation());
529
530 new_state.adjust_transforms_for_child(&geom.transform(), &geom.transform());
531
532 if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = geom {
533 new_state.clipped = new_state
534 .clipped
535 .intersection(
536 &state
537 .transform_to_screen
538 .outer_transformed_rect(&geometry.cast())
539 .cast(),
540 )
541 .unwrap_or_default();
542 }
543
544 self.mark_dirty_rect(
545 geom.bounding_rect(),
546 state.transform_to_screen,
547 &state.clipped,
548 );
549 if new_state.clipped.is_empty() {
550 ItemVisitorResult::SkipChildren
551 } else {
552 ItemVisitorResult::Continue(new_state)
553 }
554 }
555 }
556 },
557 {
558 let initial_transform =
559 euclid::Transform2D::translation(origin.x as f32, origin.y as f32);
560 ComputeDirtyRegionState {
561 transform_to_screen: initial_transform,
562 old_transform_to_screen: initial_transform,
563 clipped: LogicalRect::from_size(size),
564 must_refresh_children: false,
565 }
566 },
567 );
568 }
569
570 fn mark_dirty_rect(
571 &mut self,
572 rect: &LogicalRect,
573 transform: ItemTransform,
574 clip_rect: &LogicalRect,
575 ) {
576 #[cfg(not(slint_int_coord))]
577 if !rect.origin.is_finite() {
578 return;
580 }
581
582 if !rect.is_empty() {
583 if let Some(rect) =
584 transform.outer_transformed_rect(&rect.cast()).cast().intersection(clip_rect)
585 {
586 self.dirty_region.add_rect(rect);
587 }
588 }
589 }
590
591 fn do_rendering(
592 cache: &RefCell<PartialRendererCache>,
593 rendering_data: &CachedRenderingData,
594 item_rc: &ItemRc,
595 render_fn: impl FnOnce(),
596 ) {
597 let mut cache = cache.borrow_mut();
598 if let Some(entry) = rendering_data.get_entry(&mut cache) {
599 entry
600 .tracker
601 .get_or_insert_with(|| Box::pin(PropertyTracker::default()))
602 .as_ref()
603 .evaluate(render_fn);
604 } else {
605 item_rc.geometry();
608 render_fn();
609 }
610 }
611
612 pub fn into_inner(self) -> T {
614 self.actual_renderer
615 }
616}
617
618macro_rules! forward_rendering_call {
619 (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => {
620 fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize) $(-> $Ret)? {
621 let mut ret = None;
622 Self::do_rendering(&self.cache, &obj.cached_rendering_data, item_rc, || {
623 ret = Some(self.actual_renderer.$fn(obj, item_rc, size));
624 });
625 ret.unwrap_or_default()
626 }
627 };
628}
629
630macro_rules! forward_rendering_call2 {
631 (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => {
632 fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize, cache: &CachedRenderingData) $(-> $Ret)? {
633 let mut ret = None;
634 Self::do_rendering(&self.cache, &cache, item_rc, || {
635 ret = Some(self.actual_renderer.$fn(obj, item_rc, size, &cache));
636 });
637 ret.unwrap_or_default()
638 }
639 };
640}
641
642impl<T: ItemRenderer + ItemRendererFeatures> ItemRenderer for PartialRenderer<'_, T> {
643 fn filter_item(
644 &mut self,
645 item_rc: &ItemRc,
646 window_adapter: &Rc<dyn WindowAdapter>,
647 ) -> (bool, LogicalRect) {
648 let item = item_rc.borrow();
649
650 let item_geometry = crate::properties::evaluate_no_tracking(|| item_rc.geometry());
652
653 let rendering_data = item.cached_rendering_data_offset();
654 let mut cache = self.cache.borrow_mut();
655 let item_bounding_rect = match rendering_data.get_entry(&mut cache) {
656 Some(PartialRenderingCachedData { data, tracker: _ }) => *data.bounding_rect(),
657 None => {
658 item_rc.bounding_rect(&item_geometry, window_adapter)
660 }
661 };
662
663 let clipped_geom = self.get_current_clip().intersection(&item_bounding_rect);
664 let draw = clipped_geom.is_some_and(|clipped_geom| {
665 let clipped_geom = clipped_geom.translate(self.translation());
666 self.dirty_region.draw_intersects(clipped_geom)
667 });
668
669 (draw, item_geometry)
670 }
671
672 forward_rendering_call2!(fn draw_rectangle(dyn RenderRectangle));
673 forward_rendering_call2!(fn draw_border_rectangle(dyn RenderBorderRectangle));
674 forward_rendering_call2!(fn draw_window_background(dyn RenderRectangle));
675 forward_rendering_call2!(fn draw_image(dyn RenderImage));
676 forward_rendering_call2!(fn draw_text(dyn RenderText));
677 forward_rendering_call!(fn draw_text_input(TextInput));
678 #[cfg(feature = "std")]
679 forward_rendering_call!(fn draw_path(Path));
680 forward_rendering_call!(fn draw_box_shadow(BoxShadow));
681
682 forward_rendering_call!(fn visit_clip(Clip) -> RenderingResult);
683 forward_rendering_call!(fn visit_opacity(Opacity) -> RenderingResult);
684
685 fn combine_clip(
686 &mut self,
687 rect: LogicalRect,
688 radius: LogicalBorderRadius,
689 border_width: LogicalLength,
690 ) -> bool {
691 self.actual_renderer.combine_clip(rect, radius, border_width)
692 }
693
694 fn get_current_clip(&self) -> LogicalRect {
695 self.actual_renderer.get_current_clip()
696 }
697
698 fn translate(&mut self, distance: LogicalVector) {
699 self.actual_renderer.translate(distance)
700 }
701 fn translation(&self) -> LogicalVector {
702 self.actual_renderer.translation()
703 }
704
705 fn rotate(&mut self, angle_in_degrees: f32) {
706 self.actual_renderer.rotate(angle_in_degrees)
707 }
708
709 fn scale(&mut self, x_factor: f32, y_factor: f32) {
710 self.actual_renderer.scale(x_factor, y_factor)
711 }
712
713 fn apply_opacity(&mut self, opacity: f32) {
714 self.actual_renderer.apply_opacity(opacity)
715 }
716
717 fn save_state(&mut self) {
718 self.actual_renderer.save_state()
719 }
720
721 fn restore_state(&mut self) {
722 self.actual_renderer.restore_state()
723 }
724
725 fn scale_factor(&self) -> f32 {
726 self.actual_renderer.scale_factor()
727 }
728
729 fn draw_cached_pixmap(
730 &mut self,
731 item_rc: &ItemRc,
732 update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])),
733 ) {
734 self.actual_renderer.draw_cached_pixmap(item_rc, update_fn)
735 }
736
737 fn draw_string(&mut self, string: &str, color: crate::Color) {
738 self.actual_renderer.draw_string(string, color)
739 }
740
741 fn draw_image_direct(&mut self, image: crate::graphics::image::Image) {
742 self.actual_renderer.draw_image_direct(image)
743 }
744
745 fn window(&self) -> &crate::window::WindowInner {
746 self.actual_renderer.window()
747 }
748
749 fn as_any(&mut self) -> Option<&mut dyn core::any::Any> {
750 self.actual_renderer.as_any()
751 }
752}
753
754#[derive(Default)]
757pub struct PartialRenderingState {
758 partial_cache: RefCell<PartialRendererCache>,
759 force_dirty: RefCell<DirtyRegion>,
761 force_screen_refresh: Cell<bool>,
763}
764
765impl PartialRenderingState {
766 pub fn create_partial_renderer<T: ItemRenderer + ItemRendererFeatures>(
769 &self,
770 renderer: T,
771 ) -> PartialRenderer<'_, T> {
772 PartialRenderer::new(&self.partial_cache, self.force_dirty.take(), renderer)
773 }
774
775 pub fn apply_dirty_region<T: ItemRenderer + ItemRendererFeatures>(
780 &self,
781 partial_renderer: &mut PartialRenderer<'_, T>,
782 components: &[(ItemTreeWeak, LogicalPoint)],
783 logical_window_size: LogicalSize,
784 dirty_region_of_existing_buffer: Option<DirtyRegion>,
785 ) -> DirtyRegion {
786 for (component, origin) in components {
787 if let Some(component) = crate::item_tree::ItemTreeWeak::upgrade(component) {
788 partial_renderer.compute_dirty_regions(&component, *origin, logical_window_size);
789 }
790 }
791
792 let screen_region = LogicalRect::from_size(logical_window_size);
793
794 if self.force_screen_refresh.take() {
795 partial_renderer.dirty_region = screen_region.into();
796 }
797
798 let region_to_repaint = partial_renderer.dirty_region.clone();
799
800 partial_renderer.dirty_region = match dirty_region_of_existing_buffer {
801 Some(dirty_region) => partial_renderer.dirty_region.union(&dirty_region),
802 None => partial_renderer.dirty_region.clone(),
803 }
804 .intersection(screen_region);
805
806 region_to_repaint
807 }
808
809 pub fn mark_dirty_region(&self, region: DirtyRegion) {
811 self.force_dirty.replace_with(|r| r.union(®ion));
812 }
813
814 pub fn free_graphics_resources(&self, items: &mut dyn Iterator<Item = Pin<ItemRef<'_>>>) {
817 for item in items {
818 item.cached_rendering_data_offset().release(&mut self.partial_cache.borrow_mut());
819 }
820
821 self.force_screen_refresh.set(true)
824 }
825
826 pub fn clear_cache(&self) {
828 self.partial_cache.borrow_mut().clear();
829 }
830
831 pub fn force_screen_refresh(&self) {
833 self.force_screen_refresh.set(true);
834 }
835}
836
837#[test]
838fn dirty_region_no_intersection() {
839 let mut region = DirtyRegion::default();
840 region.add_rect(LogicalRect::new(LogicalPoint::new(10., 10.), LogicalSize::new(16., 16.)));
841 region.add_rect(LogicalRect::new(LogicalPoint::new(100., 100.), LogicalSize::new(16., 16.)));
842 region.add_rect(LogicalRect::new(LogicalPoint::new(200., 100.), LogicalSize::new(16., 16.)));
843 let i = region
844 .intersection(LogicalRect::new(LogicalPoint::new(50., 50.), LogicalSize::new(10., 10.)));
845 assert_eq!(i.iter().count(), 0);
846}