Skip to content

canvas: image_src is not on ui.ElementOptions, so apps cannot atlas and the 16-slot registry becomes the ceiling #387

Description

@sepehr-safari

The problem

ui.ElementOptions cannot set image_src, so an app cannot draw a sub-rectangle of a registered image. That makes a texture atlas impossible from app code, and the 16-slot registry becomes a hard ceiling on how many distinct images an app can show.

The renderer already supports it. Widget carries the field and the draw path consumes it:

  • src/primitives/canvas/widgets.zig:1206image_src: ?geometry.RectF = null on the widget options
  • src/primitives/canvas/widgets.zig:1289.image_src = options.image_src, passed straight through
  • src/primitives/canvas/widget_render.zig:2696.src = widget.image_src on the emitted DrawImage
  • src/primitives/canvas/widget_invalidation.zig:315 — already compared for damage

The app-facing options struct exposes only the id:

  • src/primitives/canvas/ui.zig:581image: canvas.ImageId = 0, and no image_src anywhere in ElementOptions
  • src/primitives/canvas/ui.zig:2103pub fn image(self: *Self, options: ElementOptions) Node

So the capability exists end to end except for one field on the struct the app fills in.

The fix

Add image_src: ?geometry.RectF = null to ui.ElementOptions and forward it where the other image fields are already forwarded, alongside image_fit and image_sampling. As far as I can tell nothing else has to change: the widget field, the render path and the invalidation comparison are all in place.

Markup would want an attribute too, though the Zig-view path alone is enough to unblock this, and a rect in markup may not be worth the grammar.

Why it is worth doing

It lifts the practical image ceiling without touching the fixed-capacity design. canvas_limits.zig:107 sets max_registered_canvas_images = 16, sized in its own comment for "avatar and cover-art scale". With a source crop, one 512x512 slot holds 64 avatars at 64x64, so the same 16 slots cover roughly a thousand small images. The budget stays frozen, the preallocation stays predictable, and the loud-overflow behaviour is unchanged. Nothing about the philosophy moves; the same memory just goes further.

It is the difference between viable and not for one class of app. I am building a Nostr client. A social feed shows dozens of small images at once: a face on every row, a gallery in a note, an icon per community in a sidebar. Today those compete for 16 slots app-wide, so I run an LRU over them and faces still fall back to initials on a dense screen. The workarounds all cost something real. Splitting the budget by kind strands capacity, evicting aggressively makes avatars flicker as you scroll, and drawing letter tiles instead of pictures is just not showing the image. An atlas removes the problem rather than rationing it.

It is cheap for you and it composes with what already shipped. 0.9.2 made the per-image budget app-settable through app.zon (max_image_pixel_bytes, types.zig:666), which fixed the size half of this. This is the count half, and it needs no new budget, no new manifest key and no new platform code.

It also helps the cases you already target. Sprite sheets for a toolbar, a spritemap of state icons, frames of a small animation, nine-slice art: all of them are one registration and many draws rather than one registration each.

Not asking for

A bigger registry, a dynamic one, or any change to how overflow behaves. Just the field.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions