///|
/// CPU-side geometry for one Metal frame: the walk that turns the shared
/// lowering's ops (`lower_draw_commands`, ADR 0019) into the frame's buffers.
/// This file never re-interprets `@core.DrawCommand`; it consumes the neutral
/// plan.
///
/// The match in `build_metal_range` carries no catch-all, so a new `PlanOp`
/// variant is a compile error here rather than a frame that quietly stops
/// drawing it — the same guarantee `moui_wgpu_renderer/native_visual_plan.mbt`
/// relies on. What *is* left counted is degradation: an approximation a host can
/// see in `MetalFrameReport` (blend modes, filters, rebuilt retained layers,
/// fallback-painted shader effects) rather than a silently wrong pixel.
///
/// Two siblings hold the rest of what this walk needs: `metal_geometry_wire.mbt`
/// (the byte layout and the code values MSL interprets) and
/// `metal_geometry_scopes.mbt` (the clip/filter/layer stack and pass slots).
/// Layers and filters are the reason this is one flat buffer set rather than a
/// tree: each open scope claims a pass slot, every item built inside it is
/// tagged with that slot in `owner`, and the host renders slot by slot — so the
/// item stream stays a linear array the encoder can filter without recursion.
priv struct MetalFrameGeometry {
size : @core.Size
scale_factor : Double
vertices : Array[Byte]
items : Array[Byte]
shapes : Array[Byte]
layer_records : Array[MetalLayerRecord]
mut vertex_count : Int32
mut item_count : Int32
mut shape_count : Int32
mut layer_count : Int32
mut owner : Int32
mut approximated_clips : Int32
mut clipped_away_ops : Int32
mut text_dropped_glyphs : Int32
mut text_runs_dropped : Int32
mut image_ops_unresolved : Int32
mut image_ops_pending : Int32
mut image_textures_released : Int32
mut layers_composited : Int32
mut blend_modes_unsupported : Int32
mut blend_modes_approximated : Int32
mut filter_approximations : Int32
mut retained_layers_rebuilt : Int32
mut retained_layers_cached : Int32
mut retained_layers_updated : Int32
mut retained_layers_evicted : Int32
mut shader_effects_fallback_painted : Int32
// A glyph the coverage atlas could not hold. Not a report field — the signal
// that makes rebuilding the frame into a cleared atlas worth it.
mut atlas_overflowed : Bool
}
///|
/// What a frame can reach outside the command stream: the shared neutral-text
/// cache and coverage atlas for glyphs, and the host image source plus decoded
/// texture cache for images. `context` is the FFI context handle, or 0 for the
/// CPU-only frame report, which resolves no images and counts none as refused.
/// `retained` is the cross-frame repaint-boundary cache. The report and a proof
/// frame each get a fresh one, which is why they always rebuild and always say
/// so: a cache hit is a claim about the frame before this one.
priv struct MetalFrameContext {
cache : @native_text.NativeTextCache
atlas : MetalGlyphAtlas
images : MetalImageCache
image_source : @render.HostImageSource
context : UInt64
async_images : Bool
retained : MetalRetainedCache
// Resident decoded-texture budget this frame enforces. A field rather than a
// constant at the call site so tests exercise the automatic path -- release
// before building -- instead of hand-calling the helper.
image_budget_bytes : Int32
}
///|
fn MetalFrameGeometry::new(
size : @core.Size,
scale_factor : Double,
) -> MetalFrameGeometry {
MetalFrameGeometry::{
size,
scale_factor,
vertices: [],
items: [],
shapes: [],
layer_records: [],
vertex_count: 0,
item_count: 0,
shape_count: 0,
layer_count: 0,
owner: -1,
approximated_clips: 0,
clipped_away_ops: 0,
text_dropped_glyphs: 0,
text_runs_dropped: 0,
image_ops_unresolved: 0,
image_textures_released: 0,
image_ops_pending: 0,
layers_composited: 0,
blend_modes_unsupported: 0,
blend_modes_approximated: 0,
filter_approximations: 0,
retained_layers_rebuilt: 0,
retained_layers_cached: 0,
retained_layers_updated: 0,
retained_layers_evicted: 0,
shader_effects_fallback_painted: 0,
atlas_overflowed: false,
}
}
///|
fn MetalFrameGeometry::vertex_bytes(self : MetalFrameGeometry) -> Bytes {
Bytes::from_array(self.vertices)
}
///|
fn MetalFrameGeometry::item_bytes(self : MetalFrameGeometry) -> Bytes {
Bytes::from_array(self.items)
}
///|
fn MetalFrameGeometry::shape_bytes(self : MetalFrameGeometry) -> Bytes {
Bytes::from_array(self.shapes)
}
///|
/// The offscreen pass table: 7 int32 (device frame, child item range, blend
/// code) followed by 4 float32 (the surface-to-target NDC transform) per record.
/// Slots are ordered parent-after-child, so the host walks them in reverse to
/// render every nested pass before the pass that samples it, and the main pass
/// last.
fn MetalFrameGeometry::layer_bytes(self : MetalFrameGeometry) -> Bytes {
let out : Array[Byte] = []
for record in self.layer_records {
push_metal_i32le(out, record.device_x)
push_metal_i32le(out, record.device_y)
push_metal_i32le(out, record.device_width)
push_metal_i32le(out, record.device_height)
push_metal_i32le(out, record.child_first)
push_metal_i32le(out, record.child_count)
push_metal_i32le(out, record.blend_mode)
push_metal_f32le(out, record.pass_scale_x)
push_metal_f32le(out, record.pass_scale_y)
push_metal_f32le(out, record.pass_offset_x)
push_metal_f32le(out, record.pass_offset_y)
}
Bytes::from_array(out)
}
///|
priv struct MetalPaintState {
transform : @core.Transform2D
opacity : Double
clip : @core.Rect
}
///|
fn new_metal_paint_state(size : @core.Size) -> MetalPaintState {
MetalPaintState::{
transform: @core.Transform2D::identity(),
opacity: 1.0,
clip: @core.Rect::new(x=0.0, y=0.0, width=size.width, height=size.height),
}
}
///|
/// The discarded-pass tag: items built for a layer this route will not draw are
/// owned by it, so no pass ever visits them.
let metal_owner_discarded : Int32 = -2
///|
fn build_metal_geometry(
plan : @render_common.DrawPlan,
metrics : @core.SurfaceMetrics,
frame : MetalFrameContext,
) -> MetalFrameGeometry {
let first = build_metal_frame(plan, metrics, frame)
if !first.atlas_overflowed {
return first
}
// The coverage page set filled partway through the frame, so some glyphs of a
// longer document were never placed. Clear it and build the frame again: the
// second pass gets every page and lays out the glyphs the first pass needed
// least of. Bounded at one retry — if a single frame cannot fit an empty page
// set, rebuilding again would only repeat the same shortfall,
// and the surviving `text_dropped_glyphs` is what a host sees.
// `moui_wgpu_renderer/native_visual_plan.mbt` applies the same policy to the
// same op, which is the reason it is a retry rather than an eviction scheme.
clear_metal_glyph_atlas(frame.atlas)
frame.atlas.resets = frame.atlas.resets + 1
build_metal_frame(plan, metrics, frame)
}
///|
fn build_metal_frame(
plan : @render_common.DrawPlan,
metrics : @core.SurfaceMetrics,
frame : MetalFrameContext,
) -> MetalFrameGeometry {
let size = metrics.logical_size
let geometry = MetalFrameGeometry::new(size, metrics.scale_factor)
// Reclaim before building: the textures this frame is about to ask for are the
// ones it will use, so the cold entries from earlier frames are what leave.
geometry.image_textures_released = metal_image_enforce_budget(
frame.images,
frame.context,
max_bytes=frame.image_budget_bytes,
)
ignore(
build_metal_range(
geometry,
frame,
plan.ops,
start_index=0,
stop_at_layer=false,
stop_at_filter=false,
initial_state=new_metal_paint_state(size),
),
)
// Pruned at the start of this frame by `MetalRetainedCache::begin_frame`, so
// the number travels with the frame that caused it rather than with the cache.
geometry.retained_layers_evicted = frame.retained.evicted
geometry
}
///|
/// Walk `ops` from `start_index`, drawing into whichever pass is currently open,
/// and stop at the first unmatched `PopLayer` / `PopFilter` when asked. The
/// nesting contract is the one `moui_wgpu_renderer` implements: a layer claims a
/// pass slot, its child stream is built with the parent transform, the layer
/// clip and opacity 1.0 (so overlapping children group instead of double-blend),
/// and the layer's own opacity is applied once when the slot is composited.
/// Returns the index of the terminator, or `ops.length()` for a full walk.
///
/// The match carries no catch-all, so a new `PlanOp` variant is a compile error
/// here rather than a frame that quietly stops drawing it — the same guarantee
/// `moui_wgpu_renderer/native_visual_plan.mbt` relies on.
fn build_metal_range(
geometry : MetalFrameGeometry,
frame : MetalFrameContext,
ops : Array[@render_common.PlanOp],
start_index~ : Int32,
stop_at_layer~ : Bool,
stop_at_filter~ : Bool,
stop_at_rounded_clip~ : Bool = false,
initial_state~ : MetalPaintState,
) -> Int32 {
let mut state = initial_state
let transform_stack : Array[@core.Transform2D] = []
let opacity_stack : Array[Double] = []
let clip_stack : Array[@core.Rect] = []
let mut index = start_index
while index < ops.length() {
let op = ops[index]
match op {
@render_common.Clear(_) => ()
@render_common.FillRect(rect, color) =>
append_metal_box(
geometry,
rect,
0.0,
@core.Brush::solid(color),
metal_mode_fill,
0.0,
state,
)
@render_common.StrokeRect(rect, color, width) =>
append_metal_box(
geometry,
rect,
0.0,
@core.Brush::solid(color),
metal_mode_stroke,
width,
state,
)
@render_common.FillRoundedRect(rounded, color) =>
append_metal_box(
geometry,
rounded.rect,
rounded.radius,
@core.Brush::solid(color),
metal_mode_fill,
0.0,
state,
)
@render_common.StrokeRoundedRect(rounded, color, width) =>
append_metal_box(
geometry,
rounded.rect,
rounded.radius,
@core.Brush::solid(color),
metal_mode_stroke,
width,
state,
)
@render_common.FillRoundedRectBrush(rounded, brush) =>
append_metal_box(
geometry,
rounded.rect,
rounded.radius,
brush,
metal_mode_fill,
0.0,
state,
)
@render_common.StrokeRoundedRectBrush(rounded, brush, width) =>
append_metal_box(
geometry,
rounded.rect,
rounded.radius,
brush,
metal_mode_stroke,
width,
state,
)
@render_common.DrawShadow(shadow) =>
append_metal_shadow(geometry, shadow, state)
@render_common.DrawPath(spec) => append_metal_path(geometry, spec, state)
@render_common.DrawText(run) =>
append_metal_text_run(geometry, frame, run, state)
@render_common.DrawImage(run) =>
append_metal_image(geometry, frame, run, state)
@render_common.PushClip(rect) => {
clip_stack.push(state.clip)
state = metal_state_with_clip(state, rect)
}
@render_common.PopClip => state = metal_state_pop_clip(state, clip_stack)
@render_common.PushRoundedClip(rounded) =>
// A rounded clip whose arc the composite can still describe becomes an
// offscreen pass rounded by the same SDF a `LayerMask::RoundedMask`
// uses, so the corners get clipped instead of squared off. Where the
// transform or a parent clip breaks that description the scope stays a
// bounding-rect scissor and the approximation stays counted.
match metal_exact_rounded_clip(rounded, state) {
Some(exact) =>
index = append_metal_layer(
geometry,
frame,
ops,
index,
mask=@core.LayerMask::RoundedMask(exact),
blend_mode=@core.BlendMode::SourceOver,
opacity=1.0,
filter=None,
rounded_clip_scope=true,
state,
)
None => {
clip_stack.push(state.clip)
geometry.approximated_clips = geometry.approximated_clips + 1
state = metal_state_with_clip(state, rounded.rect)
}
}
@render_common.PopRoundedClip =>
if stop_at_rounded_clip {
return index
} else {
state = metal_state_pop_clip(state, clip_stack)
}
@render_common.PushTransform(transform) => {
transform_stack.push(state.transform)
state = MetalPaintState::{
transform: multiply_metal_transform(state.transform, transform),
opacity: state.opacity,
clip: state.clip,
}
}
@render_common.PopTransform =>
match transform_stack.pop() {
Some(previous) =>
state = MetalPaintState::{
transform: previous,
opacity: state.opacity,
clip: state.clip,
}
None =>
state = MetalPaintState::{
transform: @core.Transform2D::identity(),
opacity: state.opacity,
clip: state.clip,
}
}
@render_common.PushOpacity(value) => {
opacity_stack.push(state.opacity)
state = MetalPaintState::{
transform: state.transform,
opacity: state.opacity * clamp_metal_unit(value),
clip: state.clip,
}
}
@render_common.PopOpacity =>
match opacity_stack.pop() {
Some(previous) =>
state = MetalPaintState::{
transform: state.transform,
opacity: previous,
clip: state.clip,
}
None =>
state = MetalPaintState::{
transform: state.transform,
opacity: 1.0,
clip: state.clip,
}
}
@render_common.PushLayer(layer) =>
index = append_metal_layer(
geometry,
frame,
ops,
index,
mask=layer.mask,
blend_mode=layer.blend_mode,
opacity=layer.opacity,
filter=None,
state,
)
@render_common.PushFilter(filter) =>
index = append_metal_layer(
geometry,
frame,
ops,
index,
mask=@core.LayerMask::NoMask,
blend_mode=@core.BlendMode::SourceOver,
opacity=1.0,
filter=Some(filter),
state,
)
@render_common.PopLayer => if stop_at_layer { return index }
@render_common.PopFilter => if stop_at_filter { return index }
@render_common.BeginRetainedLayer(spec) =>
// A repaint boundary is a group the framework would rather keep across
// frames, and `metal_retained.mbt` is what keeps it: the same boundary at
// the same slot and rect in the frame after the one that painted it
// composites that texture instead of rebuilding the group. Where there is
// no such entry -- the first frame, a moved or resized boundary, a revision
// bump, a different context -- this is the old behaviour and the count
// shows it: `retained_layers_rebuilt`, with `retained_layers_updated`
// separating a content change from a layout change and
// `retained_layers_evicted` the boundaries whose texture was claimed by
// something else in the gap.
index = append_metal_layer(
geometry,
frame,
ops,
index,
mask=@core.LayerMask::RectMask(spec.frame),
blend_mode=@core.BlendMode::SourceOver,
opacity=1.0,
filter=None,
retained=Some(spec),
state,
)
@render_common.EndRetainedLayer(_) => if stop_at_layer { return index }
@render_common.DrawShaderEffect(spec) => {
// The four builtins `builtin_shader_names()` declares are evaluated per
// fragment; a name outside that list has no formulation here, so it
// paints the brush the spec carries as its fallback and shows up in
// `shader_effects_fallback_painted`, which is how a host tells a fallback
// frame from an effect frame. The fallback brush also carries the two
// colours the builtins paint with, so it is never discarded.
let effect_kind = metal_shader_effect_kind(spec.name)
if effect_kind == metal_effect_none {
geometry.shader_effects_fallback_painted = geometry.shader_effects_fallback_painted +
1
}
append_metal_box(
geometry,
spec.frame,
0.0,
spec.fallback,
metal_mode_fill,
0.0,
state,
effect_kind~,
effect_amount=metal_shader_effect_amount(effect_kind, spec.uniforms),
)
}
}
index = index + 1
}
index
}
///|
fn append_metal_box(
geometry : MetalFrameGeometry,
rect : @core.Rect,
radius : Double,
brush : @core.Brush,
mode : Double,
stroke_width : Double,
state : MetalPaintState,
blur~ : Double = 0.0,
effect_kind~ : Double = metal_effect_none,
effect_amount~ : Double = 0.0,
) -> Unit {
if rect.size.width <= 0.0 || rect.size.height <= 0.0 || state.opacity <= 0.0 {
geometry.clipped_away_ops = geometry.clipped_away_ops + 1
return
}
let bounds = metal_transformed_bounds(rect, state.transform)
if state.clip.intersection(bounds).unwrap_or(empty_metal_rect()).is_empty() {
geometry.clipped_away_ops = geometry.clipped_away_ops + 1
return
}
let brush_payload = metal_box_brush(
metal_brush_payload(brush, state.opacity),
rect.origin,
)
let start = geometry.vertex_count
let corners = [
(rect.origin.x, rect.origin.y, 0.0, 0.0),
(rect.origin.x + rect.size.width, rect.origin.y, rect.size.width, 0.0),
(
rect.origin.x + rect.size.width,
rect.origin.y + rect.size.height,
rect.size.width,
rect.size.height,
),
(rect.origin.x, rect.origin.y, 0.0, 0.0),
(
rect.origin.x + rect.size.width,
rect.origin.y + rect.size.height,
rect.size.width,
rect.size.height,
),
(rect.origin.x, rect.origin.y + rect.size.height, 0.0, rect.size.height),
]
for corner in corners {
let (corner_x, corner_y, local_x, local_y) = corner
let point = transform_metal_point(state.transform, corner_x, corner_y)
push_metal_brush_vertex(
geometry,
point.x,
point.y,
local_x,
local_y,
brush_payload,
)
}
push_metal_draw(
geometry,
start,
state.clip,
metal_pipeline_visual,
metal_box_shape(
rect.size.width,
rect.size.height,
radius,
mode,
stroke_width~,
blur_radius=blur,
effect_kind~,
effect_amount~,
),
)
}
///|
fn append_metal_shadow(
geometry : MetalFrameGeometry,
shadow : @core.ShadowSpec,
state : MetalPaintState,
) -> Unit {
let blur = shadow.blur_radius.max(0.0)
let spread = shadow.spread.max(0.0)
// Three sigmas, because that is where the profile this quad now paints has
// fallen to 0.13% -- the old two-sigma growth was a margin for a ramp that
// stopped at one sigma, and clipping a Gaussian at 2 sigma leaves a visible
// edge to the box.
let grow = blur * 3.0 + spread
let rect = @core.Rect::new(
x=shadow.rect.rect.origin.x + shadow.offset.x - grow,
y=shadow.rect.rect.origin.y + shadow.offset.y - grow,
width=shadow.rect.rect.size.width + grow * 2.0,
height=shadow.rect.rect.size.height + grow * 2.0,
)
append_metal_box(
geometry,
rect,
shadow.rect.radius + grow,
@core.Brush::solid(shadow.color),
metal_mode_shadow,
grow,
state,
blur~,
)
}
///|
fn append_metal_path(
geometry : MetalFrameGeometry,
spec : @core.PathSpec,
state : MetalPaintState,
) -> Unit {
let mesh_vertices = @render_common.tessellate_path(spec).vertices()
if mesh_vertices.is_empty() || state.opacity <= 0.0 {
geometry.clipped_away_ops = geometry.clipped_away_ops + 1
return
}
let start = geometry.vertex_count
for vertex in mesh_vertices {
// The shared mesh already carries the brush each sub-mesh was filled with,
// so a path with both a gradient fill and a solid stroke keeps both.
let components = vertex.components()
let point = transform_metal_point(
state.transform,
components[0],
components[1],
)
push_metal_vertex(
geometry,
point.x,
point.y,
components[0],
components[1],
0.0,
0.0,
components[3],
components[4],
components[5],
components[6],
components[7],
components[8],
components[9],
components[10] * state.opacity,
components[11],
components[12],
components[13],
components[14] * state.opacity,
components[2],
)
}
push_metal_draw(
geometry,
start,
state.clip,
metal_pipeline_visual,
metal_box_shape(0.0, 0.0, 0.0, metal_mode_path),
)
}
///|
fn append_metal_text_run(
geometry : MetalFrameGeometry,
frame : MetalFrameContext,
run : @core.TextRun,
state : MetalPaintState,
) -> Unit {
let scale_factor = geometry.scale_factor
let scale = if scale_factor > 0.0 { scale_factor } else { 1.0 }
if state.opacity <= 0.0 {
geometry.clipped_away_ops = geometry.clipped_away_ops + 1
return
}
match frame.cache.layout_run(run, scale_factor) {
None => geometry.text_runs_dropped = geometry.text_runs_dropped + 1
Some(layout) => {
let start_extra = metal_text_align_extra(
run.align,
run.frame.size.width,
layout.size().width,
)
let vertical_extra = metal_text_vertical_extra(run.frame, layout)
let start = geometry.vertex_count
// One item per atlas page this run touches. Shelf packing only ever moves
// forward, so a run meets each page in one contiguous stretch and the vertex
// ranges need no reordering.
let mut group_start = start
let mut group_page : Int32 = -1
let brush_payload = metal_brush_payload(
@core.Brush::solid(run.color),
state.opacity,
)
for glyph in layout.glyphs() {
match ensure_metal_glyph(frame.atlas, frame.cache, glyph.key()) {
Ready(placement) =>
if placement.width > 0 && placement.height > 0 {
if group_page >= 0 && placement.page != group_page {
push_metal_text_group(geometry, group_start, state, group_page)
group_start = geometry.vertex_count
}
group_page = placement.page
let origin = transform_metal_point(
state.transform,
run.frame.origin.x +
start_extra +
glyph.x() +
placement.bearing_x / scale,
run.frame.origin.y +
vertical_extra +
layout.baseline() +
glyph.y() +
placement.bearing_y / scale,
)
let width = placement.width.to_double() / scale
let height = placement.height.to_double() / scale
let atlas = METAL_ATLAS_SIZE.to_double()
let u0 = placement.x.to_double() / atlas
let v0 = placement.y.to_double() / atlas
let u1 = (placement.x + placement.width).to_double() / atlas
let v1 = (placement.y + placement.height).to_double() / atlas
let colour = if placement.coverage_only {
brush_payload
} else {
// A colour glyph carries its own pixels; tinting it with the
// run colour would repaint the artwork.
MetalBrushPayload::{
kind: 0.0,
start: brush_payload.start,
end: brush_payload.end,
color0: @core.Color::rgba(r=1.0, g=1.0, b=1.0).multiply_alpha(
state.opacity,
),
color1: brush_payload.color1,
}
}
let corners = [
(0.0, 0.0, u0, v0),
(width, 0.0, u1, v0),
(width, height, u1, v1),
(0.0, 0.0, u0, v0),
(width, height, u1, v1),
(0.0, height, u0, v1),
]
for corner in corners {
let (offset_x, offset_y, u, v) = corner
let point = transform_metal_point(
state.transform,
origin.x + offset_x,
origin.y + offset_y,
)
push_metal_brush_vertex_uv(
geometry,
point.x,
point.y,
offset_x,
offset_y,
u,
v,
colour,
)
}
}
Missing =>
geometry.text_dropped_glyphs = geometry.text_dropped_glyphs + 1
Full => {
// The page set ran out, not the engine. That is recoverable — the
// frame is worth rebuilding into a cleared atlas — so it raises a
// signal as well as a count.
geometry.text_dropped_glyphs = geometry.text_dropped_glyphs + 1
geometry.atlas_overflowed = true
}
}
}
if group_page >= 0 {
push_metal_text_group(geometry, group_start, state, group_page)
} else {
truncate_metal_vertices(geometry, start)
}
}
}
}
///|
/// Close one page's worth of a glyph run. `texture_key` is the page index, which
/// is the only thing that says which coverage texture this item samples — the
/// vertex layout, the item layout and the shader all stay as they were.
fn push_metal_text_group(
geometry : MetalFrameGeometry,
start : Int32,
state : MetalPaintState,
page : Int32,
) -> Unit {
push_metal_draw(
geometry,
start,
state.clip,
metal_pipeline_text,
metal_box_shape(0.0, 0.0, 0.0, metal_mode_fill),
texture_key=page,
)
}
///|
/// An image run becomes one textured quad. `fit` and `src_rect` resolve through
/// the shared placement helper, so the route cannot invent its own crop rules.
fn append_metal_image(
geometry : MetalFrameGeometry,
frame : MetalFrameContext,
run : @core.ImageRun,
state : MetalPaintState,
) -> Unit {
if state.opacity <= 0.0 || run.opacity <= 0.0 {
geometry.clipped_away_ops = geometry.clipped_away_ops + 1
return
}
let resolved = if frame.async_images {
ensure_metal_image_async(
frame.images,
context=frame.context,
frame.image_source,
source=run.source,
)
} else {
ensure_metal_image_sync(
frame.images,
context=frame.context,
frame.image_source,
source=run.source,
)
}
match resolved {
None =>
if frame.async_images {
geometry.image_ops_pending = geometry.image_ops_pending + 1
} else {
geometry.image_ops_unresolved = geometry.image_ops_unresolved + 1
}
Some((key, image_size)) =>
match metal_image_uv(run, image_size) {
None =>
geometry.image_ops_unresolved = geometry.image_ops_unresolved + 1
Some((u0, v0, u1, v1)) => {
let box = metal_image_frame(run, image_size)
if state.clip
.intersection(metal_transformed_bounds(box, state.transform))
.unwrap_or(empty_metal_rect())
.is_empty() {
geometry.clipped_away_ops = geometry.clipped_away_ops + 1
return
}
let opacity = clamp_metal_unit(run.opacity) * state.opacity
let colour = @core.Color::rgba(r=1.0, g=1.0, b=1.0, a=opacity)
let start = geometry.vertex_count
let corners = [
(box.origin.x, box.origin.y, 0.0, 0.0, u0, v0),
(
box.origin.x + box.size.width,
box.origin.y,
box.size.width,
0.0,
u1,
v0,
),
(
box.origin.x + box.size.width,
box.origin.y + box.size.height,
box.size.width,
box.size.height,
u1,
v1,
),
(box.origin.x, box.origin.y, 0.0, 0.0, u0, v0),
(
box.origin.x + box.size.width,
box.origin.y + box.size.height,
box.size.width,
box.size.height,
u1,
v1,
),
(
box.origin.x,
box.origin.y + box.size.height,
0.0,
box.size.height,
u0,
v1,
),
]
for corner in corners {
let (corner_x, corner_y, local_x, local_y, u, v) = corner
let point = transform_metal_point(
state.transform,
corner_x,
corner_y,
)
push_metal_uv_vertex(
geometry,
point.x,
point.y,
local_x,
local_y,
u,
v,
colour,
)
}
push_metal_draw(
geometry,
start,
state.clip,
metal_pipeline_image,
metal_box_shape(box.size.width, box.size.height, 0.0, 0.0),
texture_key=key,
)
}
}
}
}
///|
fn metal_text_align_extra(
align : @core.TextAlign,
frame_width : Double,
text_width : Double,
) -> Double {
let extra = (frame_width - text_width).max(0.0)
match align {
@core.TextStart => 0.0
TextCenter => extra / 2.0
TextEnd => extra
}
}
///|
fn metal_text_vertical_extra(
frame : @core.Rect,
layout : @native_text.NativeTextLayout,
) -> Double {
let extra = frame.size.height - layout.size().height
if extra > 0.0 {
extra / 2.0
} else {
0.0
}
}