Repository: zng-ui/zng (language: rust)

Issue:
Implement `RepeatMode::Space`
Webrender does not implement this, see [bugzilla](https://bugzilla.mozilla.org/show_bug.cgi?id=1727798).


This might require implementing all other modes too, because  Space can be set only on the horizontal/vertical and Webrender does not have "None" mode.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c85d116d9..2bab8bb83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 # Unreleased
 
+* The view API `ReferenceFrameId` type now reserves some IDs for the view process.
 * Add `border_img` and other related properties and types.
 * **Breaking** Moved `zng::render::RepeatMode` to `zng::widget`.
 * **Breaking** Add `zng_app::render::Img::size` for the renderer image interface trait.
diff --git a/crates/zng-app/src/render.rs b/crates/zng-app/src/render.rs
index a986c273e..a6047906e 100644
--- a/crates/zng-app/src/render.rs
+++ b/crates/zng-app/src/render.rs
@@ -2813,9 +2813,10 @@ enum ReferenceFrameIdInner {
     FrameValueIndex(FrameValueKey<PxTransform>, u32),
 }
 impl ReferenceFrameIdInner {
-    const UNIQUE: u64 = 1 << 63;
-    const WIDGET: u64 = 1 << 62;
-    const FRAME_VALUE: u64 = 1 << 61;
+    const _RESERVED: u64 = 1 << 63; // view process
+    const UNIQUE: u64 = 1 << 62;
+    const WIDGET: u64 = 1 << 61;
+    const FRAME_VALUE: u64 = 1 << 60;
 }
 impl From<ReferenceFrameIdInner> for RenderReferenceFrameId {
     fn from(value: ReferenceFrameIdInner) -> Self {
diff --git a/crates/zng-view-api/src/display_list.rs b/crates/zng-view-api/src/display_list.rs
index bf85e926f..01c9e12c5 100644
--- a/crates/zng-view-api/src/display_list.rs
+++ b/crates/zng-view-api/src/display_list.rs
@@ -146,6 +146,8 @@ impl DisplayListBuilder {
         transform_style: TransformStyle,
         is_2d_scale_translation: bool,
     ) {
+        debug_assert!(key.is_app_generated());
+
         self.space_len += 1;
         self.list.push(DisplayItem::PushReferenceFrame {
             id: key,
diff --git a/crates/zng-view-api/src/types.rs b/crates/zng-view-api/src/types.rs
index 289233b0a..e6f690a6a 100644
--- a/crates/zng-view-api/src/types.rs
+++ b/crates/zng-view-api/src/types.rs
@@ -845,9 +845,16 @@ impl fmt::Debug for TransformStyle {
 
 /// Identifies a reference frame.
 ///
-/// This ID is defined by the app process.
+/// This ID is mostly defined by the app process. IDs that set the most significant
+/// bit of the second part (`id.1 & (1 << 63) != 0`) are reserved for the view process.
 #[derive(Default, Debug, Clone, Copy, serde::Deserialize, Eq, Hash, PartialEq, serde::Serialize)]
 pub struct ReferenceFrameId(pub u64, pub u64);
+impl ReferenceFrameId {
+    /// If ID does not set the bit that indicates it is generated by the view process.
+    pub fn is_app_generated(&self) -> bool {
+        self.1 & (1 << 63) == 0
+    }
+}
 
 /// Nine-patch border repeat mode.
 ///
diff --git a/crates/zng-view/src/display_list.rs b/crates/zng-view/src/display_list.rs
index 196249a73..fbe1fd6b9 100644
--- a/crates/zng-view/src/display_list.rs
+++ b/crates/zng-view/src/display_list.rs
@@ -2,13 +2,13 @@ use std::{cell::Cell, mem};
 
 use rustc_hash::FxHashMap;
 use webrender::api as wr;
-use zng_unit::{PxCornerRadius, PxRect, PxTransform, Rgba};
+use zng_unit::{Factor, Px, PxCornerRadius, PxRect, PxSideOffsets, PxSize, PxTransform, Rgba};
 use zng_view_api::{
     api_extension::{ApiExtensionId, ApiExtensionPayload},
     display_list::{DisplayItem, DisplayList, FilterOp, FrameValue, FrameValueId, FrameValueUpdate, NinePatchSource, SegmentId},
     font::{GlyphIndex, GlyphInstance},
     window::FrameId,
-    GradientStop,
+    GradientStop, ReferenceFrameId, RepeatMode,
 };
 
 use crate::px_wr::PxToWr;
@@ -117,6 +117,7 @@ pub struct SpaceAndClip {
     clip_stack: Vec<wr::ClipId>,
     clip_chain_stack: Vec<(wr::ClipChainId, usize)>,
     prim_flags: wr::PrimitiveFlags,
+    view_process_frame_id: u64,
 }
 impl SpaceAndClip {
     pub(crate) fn new(pipeline_id: wr::PipelineId) -> Self {
@@ -126,6 +127,7 @@ impl SpaceAndClip {
             clip_stack: vec![],
             clip_chain_stack: vec![],
             prim_flags: wr::PrimitiveFlags::IS_BACKFACE_VISIBLE,
+            view_process_frame_id: 0,
         }
     }
 
@@ -191,6 +193,12 @@ impl SpaceAndClip {
         self.prim_flags.set(wr::PrimitiveFlags::IS_BACKFACE_VISIBLE, visible);
     }
 
+    /// Generate a reference frame ID, unique on this list.
+    pub fn next_view_process_frame_id(&mut self) -> ReferenceFrameId {
+        self.view_process_frame_id = self.view_process_frame_id.wrapping_add(1);
+        ReferenceFrameId(self.view_process_frame_id, 1 << 63)
+    }
+
     pub(crate) fn clear(&mut self, pipeline_id: wr::PipelineId) {
         #[cfg(debug_assertions)]
         {
@@ -643,98 +651,21 @@ fn display_item_to_webrender(
             img_size,
             fill,
             slice,
-            mut repeat_horizontal,
-            mut repeat_vertical,
+            repeat_horizontal,
+            repeat_vertical,
         } => {
-            let wr_bounds = bounds.to_wr();
-            let clip = sc.clip_chain_id(wr_list);
-
-            let source = match source {
-                NinePatchSource::Image { image_id, rendering } => {
-                    wr::NinePatchBorderSource::Image(wr::ImageKey(cache.id_namespace(), image_id.get()), rendering.to_wr())
-                }
-                NinePatchSource::LinearGradient {
-                    start_point,
-                    end_point,
-                    extend_mode,
-                    stops,
-                } => {
-                    wr_list.push_stops(cast_gradient_stops_to_wr(stops));
-                    wr::NinePatchBorderSource::Gradient(wr::Gradient {
-                        start_point: start_point.cast_unit(),
-                        end_point: end_point.cast_unit(),
-                        extend_mode: extend_mode.to_wr(),
-                    })
-                }
-                NinePatchSource::RadialGradient {
-                    center,
-                    radius,
-                    start_offset,
-                    end_offset,
-                    extend_mode,
-                    stops,
-                } => {
-                    wr_list.push_stops(cast_gradient_stops_to_wr(stops));
-                    wr::NinePatchBorderSource::RadialGradient(wr::RadialGradient {
-                        center: center.cast_unit(),
-                        radius: radius.cast_unit(),
-                        start_offset: *start_offset,
-                        end_offset: *end_offset,
-                        extend_mode: extend_mode.to_wr(),
-                    })
-                }
-                NinePatchSource::ConicGradient {
-                    center,
-                    angle,
-                    start_offset,
-                    end_offset,
-                    extend_mode,
-                    stops,
-                } => {
-                    wr_list.push_stops(cast_gradient_stops_to_wr(stops));
-                    wr::NinePatchBorderSource::ConicGradient(wr::ConicGradient {
-                        center: center.cast_unit(),
-                        angle: angle.0,
-                        start_offset: *start_offset,
-                        end_offset: *end_offset,
-                        extend_mode: extend_mode.to_wr(),
-                    })
-                }
-            };
-
-            // webrender does not implement RepeatMode::Space
-            let mut space_mode = false;
-            if matches!(repeat_horizontal, zng_view_api::RepeatMode::Space) {
-                repeat_horizontal = zng_view_api::RepeatMode::Repeat;
-                space_mode = true;
-            }
-            if matches!(repeat_vertical, zng_view_api::RepeatMode::Space) {
-                repeat_vertical = zng_view_api::RepeatMode::Repeat;
-                space_mode = true;
-            }
-
-            if space_mode {
-                tracing::warn!("`RepeatMode::Space` not implemented, will use `RepeatMode::Repeat`");
-            }
-
-            wr_list.push_border(
-                &wr::CommonItemProperties {
-                    clip_rect: wr_bounds,
-                    clip_chain_id: clip,
-                    spatial_id: sc.spatial_id(),
-                    flags: sc.primitive_flags(),
-                },
-                wr_bounds,
-                widths.to_wr(),
-                wr::BorderDetails::NinePatch(wr::NinePatchBorder {
-                    source,
-                    width: img_size.width.0,
-                    height: img_size.height.0,
-                    slice: slice.to_wr_device(),
-                    fill: *fill,
-                    repeat_horizontal: repeat_horizontal.to_wr(),
-                    repeat_vertical: repeat_vertical.to_wr(),
-                }),
+            nine_patch_border_to_webrender(
+                sc,
+                wr_list,
+                source,
+                cache,
+                *bounds,
+                *widths,
+                *repeat_horizontal,
+                *slice,
+                *img_size,
+                *repeat_vertical,
+                *fill,
             );
         }
 
@@ -940,6 +871,381 @@ fn display_item_to_webrender(
     }
 }
 
+#[allow(clippy::too_many_arguments)]
+fn nine_patch_border_to_webrender(
+    sc: &mut SpaceAndClip,
+    wr_list: &mut wr::DisplayListBuilder,
+    source: &NinePatchSource,
+    cache: &DisplayListCache,
+    mut bounds: PxRect,
+    mut widths: PxSideOffsets,
+    repeat_horizontal: RepeatMode,
+    slice: PxSideOffsets,
+    img_size: PxSize,
+    repeat_vertical: RepeatMode,
+    fill: bool,
+) {
+    let clip = sc.clip_chain_id(wr_list);
+
+    let source = match source {
+        NinePatchSource::Image { image_id, rendering } => {
+            wr::NinePatchBorderSource::Image(wr::ImageKey(cache.id_namespace(), image_id.get()), rendering.to_wr())
+        }
+        NinePatchSource::LinearGradient {
+            start_point,
+            end_point,
+            extend_mode,
+            stops,
+        } => {
+            wr_list.push_stops(cast_gradient_stops_to_wr(stops));
+            wr::NinePatchBorderSource::Gradient(wr::Gradient {
+                start_point: start_point.cast_unit(),
+                end_point: end_point.cast_unit(),
+                extend_mode: extend_mode.to_wr(),
+            })
+        }
+        NinePatchSource::RadialGradient {
+            center,
+            radius,
+            start_offset,
+            end_offset,
+            extend_mode,
+            stops,
+        } => {
+            wr_list.push_stops(cast_gradient_stops_to_wr(stops));
+            wr::NinePatchBorderSource::RadialGradient(wr::RadialGradient {
+                center: center.cast_unit(),
+                radius: radius.cast_unit(),
+                start_offset: *start_offset,
+                end_offset: *end_offset,
+                extend_mode: extend_mode.to_wr(),
+            })
+        }
+        NinePatchSource::ConicGradient {
+            center,
+            angle,
+            start_offset,
+            end_offset,
+            extend_mode,
+            stops,
+        } => {
+            wr_list.push_stops(cast_gradient_stops_to_wr(stops));
+            wr::NinePatchBorderSource::ConicGradient(wr::ConicGradient {
+                center: center.cast_unit(),
+                angle: angle.0,
+                start_offset: *start_offset,
+                end_offset: *end_offset,
+                extend_mode: extend_mode.to_wr(),
+            })
+        }
+    };
+
+    // webrender does not implement RepeatMode::Space, so we hide the space lines (and corners) and do a manual repeat
+    let actual_bounds = bounds;
+    let actual_widths = widths;
+    let mut render_corners = false;
+    if let wr::NinePatchBorderSource::Image(image_key, rendering) = source {
+        use wr::euclid::rect as r;
+
+        if matches!(repeat_horizontal, zng_view_api::RepeatMode::Space) {
+            bounds.origin.y += widths.top;
+            bounds.size.height -= widths.vertical();
+            widths.top = Px(0);
+            widths.bottom = Px(0);
+            render_corners = true;
+
+            for (bounds, slice) in [
+                // top
+                (
+                    r::<_, Px>(
+                        actual_widths.left,
+                        Px(0),
+                        actual_bounds.width() - actual_widths.horizontal(),
+                        actual_widths.top,
+                    ),
+                    r::<_, Px>(slice.left, Px(0), img_size.width - slice.horizontal(), slice.top),
+                ),
+                // bottom
+                (
+                    r(
+                        actual_widths.left,
+                        actual_bounds.height() - actual_widths.bottom,
+                        actual_bounds.width() - actual_widths.horizontal(),
+                        actual_widths.bottom,
+                    ),
+                    r(
+                        slice.left,
+                        img_size.height - slice.bottom,
+                        img_size.width - slice.horizontal(),
+                        slice.bottom,
+                    ),
+                ),
+            ] {
+                let scale = Factor(bounds.height().0 as f32 / slice.height().0 as f32);
+
+                let size = PxRect::from_size(img_size * scale).to_wr();
+                let clip = slice * scale;
+
+                let mut offset_x = (bounds.origin.x - clip.origin.x).0 as f32;
+                let offset_y = (bounds.origin.y - clip.origin.y).0 as f32;
+
+                let bounds_width = bounds.width().0 as f32;
+                let clip = clip.to_wr();
+                let n = (bounds_width / clip.width()).floor();
+                let space = bounds_width - clip.width() * n;
+                let space = space / (n + 1.0);
+
+                offset_x += space;
+                let advance = clip.width() + space;
+                for _ in 0..n as u32 {
+                    let spatial_id = wr_list.push_reference_frame(
+                        wr::units::LayoutPoint::zero(),
+                        sc.spatial_id(),
+                        wr::TransformStyle::Flat,
+                        wr::PropertyBinding::Value(wr::units::LayoutTransform::translation(offset_x, offset_y, 0.0)),
+                        wr::ReferenceFrameKind::Transform {
+                            is_2d_scale_translation: true,
+                            should_snap: false,
+                            paired_with_perspective: false,
+                        },
+                        sc.next_view_process_frame_id().to_wr(),
+                    );
+                    sc.push_spatial(spatial_id);
+
+                    let clip_id = sc.clip_chain_id(wr_list);
+                    wr_list.push_image(
+                        &wr::CommonItemProperties {
+                            clip_rect: clip,
+                            clip_chain_id: clip_id,
+                            spatial_id: sc.spatial_id(),
+                            flags: sc.primitive_flags(),
+                        },
+                        size,
+                        rendering,
+                        wr::AlphaType::Alpha,
+                        image_key,
+                        wr::ColorF::WHITE,
+                    );
+
+                    wr_list.pop_reference_frame();
+                    sc.pop_spatial();
+
+                    offset_x += advance;
+                }
+            }
+        }
+        if matches!(repeat_vertical, zng_view_api::RepeatMode::Space) {
+            bounds.origin.x += widths.left;
+            bounds.size.width -= widths.horizontal();
+            widths.left = Px(0);
+            widths.right = Px(0);
+            render_corners = true;
+
+            for (bounds, slice) in [
+                // left
+                (
+                    r::<_, Px>(
+                        Px(0),
+                        actual_widths.top,
+                        actual_widths.left,
+                        actual_bounds.height() - actual_widths.vertical(),
+                    ),
+                    r::<_, Px>(Px(0), slice.top, slice.left, img_size.height - slice.vertical()),
+                ),
+                // right
+                (
+                    r(
+                        actual_bounds.width() - actual_widths.right,
+                        actual_widths.top,
+                        actual_widths.right,
+                        actual_bounds.height() - actual_widths.vertical(),
+                    ),
+                    r(
+                        img_size.width - slice.right,
+                        slice.left,
+                        slice.right,
+                        img_size.height - slice.vertical(),
+                    ),
+                ),
+            ] {
+                let scale = Factor(bounds.width().0 as f32 / slice.width().0 as f32);
+
+                let size = PxRect::from_size(img_size * scale).to_wr();
+                let clip = slice * scale;
+
+                let offset_x = (bounds.origin.x - clip.origin.x).0 as f32;
+                let mut offset_y = (bounds.origin.y - clip.origin.y).0 as f32;
+
+                let bounds_height = bounds.height().0 as f32;
+                let clip = clip.to_wr();
+                let n = (bounds_height / clip.height()).floor();
+                let space = bounds_height - clip.height() * n;
+                let space = space / (n + 1.0);
+
+                offset_y += space;
+                let advance = clip.height() + space;
+                for _ in 0..n as u32 {
+                    let spatial_id = wr_list.push_reference_frame(
+                        wr::units::LayoutPoint::zero(),
+                        sc.spatial_id(),
+                        wr::TransformStyle::Flat,
+                        wr::PropertyBinding::Value(wr::units::LayoutTransform::translation(offset_x, offset_y, 0.0)),
+                        wr::ReferenceFrameKind::Transform {
+                            is_2d_scale_translation: true,
+                            should_snap: false,
+                            paired_with_perspective: false,
+                        },
+                        sc.next_view_process_frame_id().to_wr(),
+                    );
+                    sc.push_spatial(spatial_id);
+
+                    let clip_id = sc.clip_chain_id(wr_list);
+                    wr_list.push_image(
+                        &wr::CommonItemProperties {
+                            clip_rect: clip,
+                            clip_chain_id: clip_id,
+                            spatial_id: sc.spatial_id(),
+                            flags: sc.primitive_flags(),
+                        },
+                        size,
+                        rendering,
+                        wr::AlphaType::Alpha,
+                        image_key,
+                        wr::ColorF::WHITE,
+                    );
+
+                    wr_list.pop_reference_frame();
+                    sc.pop_spatial();
+
+                    offset_y += advance;
+                }
+            }
+        }
+    }
+
+    let wr_bounds = bounds.to_wr();
+
+    wr_list.push_border(
+        &wr::CommonItemProperties {
+            clip_rect: wr_bounds,
+            clip_chain_id: clip,
+            spatial_id: sc.spatial_id(),
+            flags: sc.primitive_flags(),
+        },
+        wr_bounds,
+        widths.to_wr(),
+        wr::BorderDetails::NinePatch(wr::NinePatchBorder {
+            source,
+            width: img_size.width.0,
+            height: img_size.height.0,
+            slice: slice.to_wr_device(),
+            fill,
+            repeat_horizontal: repeat_horizontal.to_wr(),
+            repeat_vertical: repeat_vertical.to_wr(),
+        }),
+    );
+
+    // if we rendered RepeatMode::Space
+    if render_corners {
+        let wr::NinePatchBorderSource::Image(image_key, rendering) = source else {
+            unreachable!()
+        };
+
+        use wr::euclid::rect as r;
+
+        for (bounds, slice) in [
+            // top-left
+            (
+                r::<_, Px>(Px(0), Px(0), actual_widths.left, actual_widths.top),
+                r::<_, Px>(Px(0), Px(0), slice.left, slice.top),
+            ),
+            // top-right
+            (
+                r(
+                    actual_bounds.width() - actual_widths.right,
+                    Px(0),
+                    actual_widths.right,
+                    actual_widths.top,
+                ),
+                r(img_size.width - slice.right, Px(0), slice.right, slice.top),
+            ),
+            // bottom-right
+            (
+                r(
+                    actual_bounds.width() - actual_widths.right,
+                    actual_bounds.height() - actual_widths.bottom,
+                    actual_widths.right,
+                    actual_widths.bottom,
+                ),
+                r(
+                    img_size.width - slice.right,
+                    img_size.height - slice.bottom,
+                    slice.right,
+                    slice.bottom,
+                ),
+            ),
+            // bottom-left
+            (
+                r(
+                    Px(0),
+                    actual_bounds.height() - actual_widths.bottom,
+                    actual_widths.left,
+                    actual_widths.bottom,
+                ),
+                r(Px(0), img_size.height - slice.bottom, slice.left, slice.bottom),
+            ),
+        ] {
+            let scale_x = bounds.size.width.0 as f32 / slice.size.width.0 as f32;
+            let scale_y = bounds.size.height.0 as f32 / slice.size.height.0 as f32;
+
+            let mut size = img_size;
+            size.width *= scale_x;
+            size.height *= scale_y;
+
+            let mut clip = slice;
+            clip.origin.x *= scale_x;
+            clip.origin.y *= scale_y;
+            clip.size.width *= scale_x;
+            clip.size.height *= scale_y;
+
+            let offset_x = bounds.origin.x - clip.origin.x;
+            let offset_y = bounds.origin.y - clip.origin.y;
+
+            let spatial_id = wr_list.push_reference_frame(
+                wr::units::LayoutPoint::zero(),
+                sc.spatial_id(),
+                wr::TransformStyle::Flat,
+                wr::PropertyBinding::Value(wr::units::LayoutTransform::translation(offset_x.0 as _, offset_y.0 as _, 0.0)),
+                wr::ReferenceFrameKind::Transform {
+                    is_2d_scale_translation: true,
+                    should_snap: false,
+                    paired_with_perspective: false,
+                },
+                sc.next_view_process_frame_id().to_wr(),
+            );
+            sc.push_spatial(spatial_id);
+
+            let clip_id = sc.clip_chain_id(wr_list);
+            wr_list.push_image(
+                &wr::CommonItemProperties {
+                    clip_rect: clip.to_wr(),
+                    clip_chain_id: clip_id,
+                    spatial_id: sc.spatial_id(),
+                    flags: sc.primitive_flags(),
+                },
+                PxRect::from_size(size).to_wr(),
+                rendering,
+                wr::AlphaType::Alpha,
+                image_key,
+                wr::ColorF::WHITE,
+            );
+
+            wr_list.pop_reference_frame();
+            sc.pop_spatial();
+        }
+    }
+}
+
 fn display_item_register_bindings(item: &DisplayItem, bindings: &mut FxHashMap<FrameValueId, (FrameId, usize)>, value: (FrameId, usize)) {
     match item {
         DisplayItem::PushReferenceFrame {
diff --git a/examples/border/res/border.png b/examples/border/res/border.png
index e84870e6c..1fabb5288 100644
Binary files a/examples/border/res/border.png and b/examples/border/res/border.png differ
diff --git a/examples/border/src/main.rs b/examples/border/src/main.rs
index df9347185..52067a696 100644
--- a/examples/border/src/main.rs
+++ b/examples/border/src/main.rs
@@ -97,18 +97,20 @@ fn app_main() {
 fn on_border_img() {
     WINDOWS.focus_or_open("border_img-win", async {
         let fill = var(false);
-        let repeat = var(zng::widget::RepeatMode::default());
+        let repeat = var(zng::widget::BorderRepeats::default());
         Window! {
             title = "border_img";
             child = Container! {
                 widget::border_img = {
                     widths: 15,
-                    source: zng::env::res("border.png"),
+                    source: zng::env::res("border-test.png"),
                     slices: (100.0 / 3.0).pct(),
                 };
                 widget::border_img_fill = fill.clone();
                 widget::border_img_repeat = repeat.map_into();
 
+                widget::foreground_highlight = -15, 15, colors::RED.with_alpha(30.pct());
+
                 layout::margin = 20;
                 padding = 20;
                 child = Stack!(top_to_bottom, ui_vec![
@@ -126,13 +128,15 @@ fn on_border_img() {
                         children = {
                             use zng::widget::RepeatMode::*;
                             [
-                                Stretch,
-                                Repeat,
-                                Round,
-                                Space, // not implemented by zng-view yet
+                                (Stretch, Stretch),
+                                (Repeat, Repeat),
+                                (Round, Round),
+                                (Space, Space),
+                                (Stretch, Space),
+                                (Space, Stretch),
                             ].into_iter().map(|m| Toggle! {
-                                value::<zng::widget::RepeatMode> = m;
-                                child = Text!("{m:?};");
+                                value::<zng::widget::BorderRepeats> = m;
+                                child = if m.0 == m.1 { Text!("{:?};", m.0) } else { Text!("{m:?};") };
                                 style_fn = toggle::RadioStyle!();
                             }).collect::<UiVec>()
                         }


Test patch:
diff --git a/examples/border/res/border-test.png b/examples/border/res/border-test.png
new file mode 100644
index 000000000..beef677af
Binary files /dev/null and b/examples/border/res/border-test.png differ