Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527//! Metal GPU renderer: translates display list paint commands into batched//! Metal draw calls for hardware-accelerated rendering.//!//! This is the production rendering path. Walks the `DisplayList` in painter's//! order, converts each `PaintCommand` into GPU vertex data, and submits//! batched draw calls via the Metal render pipeline.//!//! Supports compositing layers: elements with `opacity < 1.0` (or promoted via//! `will-change`) are rendered to intermediate textures and composited back//! with the correct group opacity.
use std::collections::HashMap;
use we_css::values::{ Color, GradientStop, LinearGradient, LinearGradientDirection, RadialGradient,};use we_dom::NodeId;use we_image::pixel::Image;use we_platform::metal::{ self, ClearColor, ClipParams, CommandBuffer, CommandQueue, Device, MetalLayer, MetalRenderer, RenderCommandEncoder, SamplerState, Texture, TextureCache, Uniforms, Vertex, MTL_PIXEL_FORMAT_BGRA8_UNORM, MTL_PRIMITIVE_TYPE_TRIANGLE, TEXTURE_MODE_COLOR, TEXTURE_MODE_MASK, TEXTURE_MODE_SDF, TEXTURE_MODE_SHADOW, TEXTURE_MODE_SOLID,};use we_style::computed::{BackgroundRepeat, LengthOrAuto, ObjectFit};use we_text::font::{Font, FontRegistry};
use crate::atlas::{GlyphAtlas, TexturedQuad};use crate::{DisplayList, PaintCommand};
// ---------------------------------------------------------------------------// Clip rect (integer, for Metal scissor rects)// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy)]struct ScissorRect { x: u32, y: u32, width: u32, height: u32,}
impl ScissorRect { fn full(viewport_width: u32, viewport_height: u32) -> Self { ScissorRect { x: 0, y: 0, width: viewport_width, height: viewport_height, } }
fn intersect(self, other: ScissorRect) -> ScissorRect { let x0 = self.x.max(other.x); let y0 = self.y.max(other.y); let x1 = (self.x + self.width).min(other.x + other.width); let y1 = (self.y + self.height).min(other.y + other.height); if x1 <= x0 || y1 <= y0 { ScissorRect { x: 0, y: 0, width: 0, height: 0, } } else { ScissorRect { x: x0, y: y0, width: x1 - x0, height: y1 - y0, } } }}
// ---------------------------------------------------------------------------// Texture key// ---------------------------------------------------------------------------
/// Identifies which texture a batch uses.#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]enum TextureKey { /// Solid-color quads — bind the 1x1 dummy white texture. Solid, /// Glyph atlas page. AtlasPage(usize), /// Image node texture. Image(usize),}
/// Font inputs used by the GPU text renderer.#[derive(Clone, Copy)]pub struct RenderFonts<'a> { pub base: &'a Font, pub registry: Option<&'a FontRegistry>,}
impl<'a> RenderFonts<'a> { pub fn new(base: &'a Font, registry: Option<&'a FontRegistry>) -> Self { RenderFonts { base, registry } }}
#[derive(Clone, Hash, PartialEq, Eq)]struct FontKey { family_list: String, weight: u16, italic: bool,}
struct FontResolver<'a> { registry: Option<&'a FontRegistry>, fallback: &'a Font, cache: HashMap<FontKey, Font>,}
impl<'a> FontResolver<'a> { fn new(registry: Option<&'a FontRegistry>, fallback: &'a Font) -> Self { FontResolver { registry, fallback, cache: HashMap::new(), } }
fn resolve(&mut self, family_list: &str, font_weight: f32, italic: bool) -> &Font { let Some(registry) = self.registry else { return self.fallback; }; if family_list.trim().is_empty() { return self.fallback; }
let weight = normalize_font_weight(font_weight); let key = FontKey { family_list: family_list.to_string(), weight, italic, }; if !self.cache.contains_key(&key) { if let Some(font) = registry.find_font_for_family_list_with_weight(family_list, weight, italic) { self.cache.insert(key.clone(), font); } }
self.cache.get(&key).unwrap_or(self.fallback) }}
fn normalize_font_weight(weight: f32) -> u16 { if !weight.is_finite() { return 400; } weight.round().clamp(1.0, 1000.0) as u16}
// ---------------------------------------------------------------------------// Compositing layer state// ---------------------------------------------------------------------------
/// Saved state for a compositing layer that is being rendered to an/// intermediate texture.struct LayerState { /// `true` if this layer does not need an intermediate texture (opacity=1.0, /// no isolation needed). Content is rendered directly to the parent target. passthrough: bool, /// Offscreen render target texture (only for non-passthrough layers). This /// is the single-sample resolve target that the layer's MSAA samples are /// resolved into, and that is later sampled when compositing back. texture: Option<Texture>, /// Multisample companion for `texture`. Held for the layer's lifetime so the /// MSAA samples survive any Load passes (e.g. when a nested sub-layer pops /// and the parent layer is reopened). msaa_texture: Option<Texture>, /// Layer bounds in parent screen coordinates. bounds_x: f32, bounds_y: f32, bounds_w: f32, bounds_h: f32, /// Layer opacity (applied when compositing back to parent). opacity: f32, /// Saved parent render target texture Id. parent_target_id: we_platform::objc::Id, /// Saved parent MSAA companion texture Id. parent_msaa_id: we_platform::objc::Id, /// Saved parent viewport dimensions (pixels). parent_vw: u32, parent_vh: u32, /// Saved parent coordinate offset. parent_coord_offset: (f32, f32), /// Saved parent clip stack. saved_clip_stack: Vec<ScissorRect>, /// Saved parent scissor rect. saved_scissor: ScissorRect, /// Saved parent rounded-clip shape stack. saved_clip_shape_stack: Vec<ClipParams>, /// Saved parent active rounded-clip shape. saved_clip_shape: ClipParams,}
// ---------------------------------------------------------------------------// GpuRenderer// ---------------------------------------------------------------------------
/// Metal GPU renderer that processes a `DisplayList` into batched draw calls.////// Owns a glyph atlas and image texture cache to avoid redundant GPU uploads./// Supports compositing layers for correct group opacity rendering.pub struct GpuRenderer { renderer: MetalRenderer, atlas: GlyphAtlas, image_cache: TextureCache, /// GPU textures for each atlas page. atlas_textures: Vec<Option<Texture>>, /// Linear sampler for raster images. CSS images are sampled from their /// decoded level-0 pixels; generated Metal mipmaps noticeably diverge from /// browser image resampling on real-web photographic backgrounds. image_sampler: SamplerState, /// Nearest sampler for R8 glyph masks. Glyph bitmaps are already /// supersampled at their target device-pixel size; linear filtering would /// blend mask edges against the atlas padding and thin text. mask_sampler: SamplerState, /// Device pixels per logical point (Retina backing scale). The display /// list stays in logical coordinates; this scales the drawable, scissor /// rects, and glyph rasterization so content is rendered at device /// resolution instead of being upscaled by the compositor. scale: f32, /// Cached MSAA companion texture for the on-screen drawable, sized in device /// pixels. The pipeline rasterizes into this 4×-multisampled target and /// resolves into the drawable on store. Recreated when the drawable size /// changes. (`render_to_pixels` builds its own per-call MSAA texture.) screen_msaa: Option<Texture>,}
impl GpuRenderer { /// Create a new GPU renderer on the given Metal device. /// /// Returns `None` if Metal pipeline creation fails. pub fn new(device: Device) -> Option<Self> { let image_sampler = device.new_sampler_state()?; let mask_sampler = device.new_sampler_state_nearest()?; let renderer = MetalRenderer::new(device)?; Some(GpuRenderer { renderer, atlas: GlyphAtlas::new(), image_cache: TextureCache::new(), atlas_textures: Vec::new(), image_sampler, mask_sampler, scale: 1.0, screen_msaa: None, }) }
/// Set the device backing scale (e.g. 2.0 on Retina). The caller must size /// the Metal drawable to `logical_size × scale` to match. pub fn set_scale(&mut self, scale: f32) { self.scale = scale.max(1.0); }
/// Current device backing scale. pub fn scale(&self) -> f32 { self.scale }
/// Render a display list to the Metal layer. /// /// Gets the next drawable from the layer, encodes all paint commands as /// batched GPU draw calls (including compositing layers for elements with /// opacity < 1.0), and presents the result. #[allow(clippy::too_many_arguments)] pub fn render( &mut self, display_list: &DisplayList, fonts: RenderFonts<'_>, images: &HashMap<NodeId, &Image>, layer: &MetalLayer, queue: &CommandQueue, clear_color: ClearColor, viewport_width: f32, viewport_height: f32, ) { let vw = viewport_width as u32; let vh = viewport_height as u32; if vw == 0 || vh == 0 { return; }
// Get next drawable from the layer. let drawable = match layer.next_drawable() { Some(d) => d, None => return, }; let texture_id = match metal::drawable_texture(drawable) { Some(t) => t, None => return, };
// Create command buffer — all render passes will be encoded here. let cmd_buf = match queue.command_buffer() { Some(b) => b, None => return, };
// Ensure an MSAA companion sized to the device-pixel drawable. The // display list is in logical points; the drawable is `logical × scale`. let dw = (viewport_width * self.scale).round().max(1.0) as u32; let dh = (viewport_height * self.scale).round().max(1.0) as u32; self.ensure_screen_msaa(dw, dh); let msaa_id = match self.screen_msaa.as_ref() { Some(t) => t.id(), None => return, };
// Process the display list, creating/ending encoders as needed for layers. self.encode_display_list_with_layers( display_list, fonts, images, &cmd_buf, texture_id, msaa_id, clear_color, vw, vh, );
cmd_buf.present_drawable(drawable); cmd_buf.commit(); }
/// Render a display list offscreen and read the result back to CPU memory /// as tightly-packed BGRA8 (`width * height * 4` bytes). /// /// This is the headless rendering path — same Metal pipeline as the /// on-screen `render`, but targeting a CPU-readable texture instead of a /// drawable. Used by the e2e harness and iframe snapshots so tests exercise /// the exact production renderer. Returns `None` if Metal resource creation /// fails. `width`/`height` are in pixels; callers wanting Retina-scale /// output should size accordingly and `set_scale` to match. pub fn render_to_pixels( &mut self, display_list: &DisplayList, fonts: RenderFonts<'_>, images: &HashMap<NodeId, &Image>, width: u32, height: u32, clear_color: ClearColor, ) -> Option<Vec<u8>> { if width == 0 || height == 0 { return None; } let device = self.renderer.device(); let target = device.new_readback_target_texture(width, height)?; // MSAA rasterization target resolved into `target` on store. Kept alive // until after the GPU work completes below. let msaa = device.new_msaa_texture(width, height, self.renderer.sample_count())?; let queue = device.new_command_queue()?; let cmd_buf = queue.command_buffer()?;
self.encode_display_list_with_layers( display_list, fonts, images, &cmd_buf, target.id(), msaa.id(), clear_color, width, height, );
cmd_buf.commit(); cmd_buf.wait_until_completed(); Some(target.get_bytes_bgra()) }
/// Process the display list into Metal draw calls, handling compositing /// layers by rendering to intermediate textures when needed. #[allow(clippy::too_many_arguments)] fn encode_display_list_with_layers( &mut self, display_list: &DisplayList, fonts: RenderFonts<'_>, images: &HashMap<NodeId, &Image>, cmd_buf: &CommandBuffer, initial_target_id: we_platform::objc::Id, initial_msaa_id: we_platform::objc::Id, clear_color: ClearColor, initial_vw: u32, initial_vh: u32, ) { // Create the initial render pass: rasterize into the MSAA target and // resolve into the drawable/readback texture on store. let desc = match metal::make_clear_pass_descriptor_msaa( initial_msaa_id, initial_target_id, clear_color, ) { Some(d) => d, None => return, }; let mut encoder = match cmd_buf.render_command_encoder(desc) { Some(e) => e, None => return, }; self.setup_encoder(&encoder, initial_vw as f32, initial_vh as f32);
// Current render target state. let mut current_target_id = initial_target_id; // MSAA companion for the current resolve target, kept in lockstep with // `current_target_id`. Every pass binds this as the color attachment and // resolves into `current_target_id`. let mut current_msaa_id = initial_msaa_id; let mut current_vw = initial_vw; let mut current_vh = initial_vh;
// Device backing scale. Uniforms and `current_vw/vh` stay in logical // points (so the vertex shader maps the logical display list across the // device-sized drawable, rasterizing geometry at full device // resolution). Scissor rects, however, are in device pixels, so all // clip coordinates are multiplied by `scale` via `dev`. let scale = self.scale; let dev = |v: u32| (v as f32 * scale).round() as u32;
// Coordinate offset: subtracted from display list positions when rendering // to a layer texture (maps screen coords → layer-local coords). let mut coord_offset: (f32, f32) = (0.0, 0.0);
// Clip stack and scissor state for the current render target (device px). let full_scissor = ScissorRect::full(dev(current_vw), dev(current_vh)); let mut clip_stack: Vec<ScissorRect> = vec![full_scissor]; let mut current_scissor = full_scissor;
// Rounded-clip shape stack, parallel to `clip_stack`. Each entry is the // active rounded clip in *logical* coordinates (matching the shader's // `world_pos`). A rectangular clip inherits its parent's rounded shape so // a `border-radius` container's corners keep clipping descendants even // through nested rectangular clips. `current_clip` is bound to the // fragment shader; `setup_encoder` binds a disabled clip by default. let mut clip_shape_stack: Vec<ClipParams> = vec![ClipParams::disabled()]; let mut current_clip = ClipParams::disabled();
// Layer stack for nested compositing layers. let mut layer_stack: Vec<LayerState> = Vec::new();
// Current batch of vertices and their texture key. let mut batch_vertices: Vec<Vertex> = Vec::with_capacity(4096); let mut batch_texture: TextureKey = TextureKey::Solid; let mut font_resolver = FontResolver::new(fonts.registry, fonts.base);
for cmd in display_list { match cmd { PaintCommand::PushLayer { opacity, x, y, width, height, } => { let needs_texture = *opacity < 1.0;
if needs_texture { // Flush and end current encoder before switching targets. if !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); } encoder.end_encoding();
// Compute layer texture dimensions (in pixels). let adjusted_x = *x + coord_offset.0; let adjusted_y = *y + coord_offset.1; let layer_w = (*width).ceil().max(1.0) as u32; let layer_h = (*height).ceil().max(1.0) as u32;
// Save parent state. layer_stack.push(LayerState { passthrough: false, texture: None, // filled below msaa_texture: None, // filled below bounds_x: adjusted_x, bounds_y: adjusted_y, bounds_w: *width, bounds_h: *height, opacity: *opacity, parent_target_id: current_target_id, parent_msaa_id: current_msaa_id, parent_vw: current_vw, parent_vh: current_vh, parent_coord_offset: coord_offset, saved_clip_stack: clip_stack.clone(), saved_scissor: current_scissor, saved_clip_shape_stack: clip_shape_stack.clone(), saved_clip_shape: current_clip, });
// Create the offscreen resolve target plus its MSAA // companion, both at device resolution. let device = self.renderer.device(); let sample_count = self.renderer.sample_count(); let offscreen = device.new_render_target_texture(dev(layer_w), dev(layer_h)); let offscreen_msaa = device.new_msaa_texture(dev(layer_w), dev(layer_h), sample_count); if let (Some(offscreen), Some(offscreen_msaa)) = (offscreen, offscreen_msaa) { let transparent = ClearColor::new(0.0, 0.0, 0.0, 0.0); let off_desc = metal::make_clear_pass_descriptor_msaa( offscreen_msaa.id(), offscreen.id(), transparent, ); if let Some(desc) = off_desc { if let Some(enc) = cmd_buf.render_command_encoder(desc) { // Switch to the layer's render target. encoder = enc; self.setup_encoder(&encoder, layer_w as f32, layer_h as f32);
// Update state for the layer. current_target_id = offscreen.id(); current_msaa_id = offscreen_msaa.id(); current_vw = layer_w; current_vh = layer_h;
// Offset coordinates so screen positions map // to layer-local positions. coord_offset.0 -= *x; coord_offset.1 -= *y;
// Reset clip stack for the new target (device px). let layer_full = ScissorRect::full(dev(layer_w), dev(layer_h)); clip_stack = vec![layer_full]; current_scissor = layer_full;
// Reset rounded-clip shape for the new target; // `setup_encoder` already bound a disabled clip. clip_shape_stack = vec![ClipParams::disabled()]; current_clip = ClipParams::disabled();
// Store both textures in the layer state so // they outlive the layer's passes. if let Some(state) = layer_stack.last_mut() { state.texture = Some(offscreen); state.msaa_texture = Some(offscreen_msaa); }
continue; } } } // If render target creation failed, fall back: reopen // the parent target with Load action and treat as passthrough. if let Some(state) = layer_stack.last_mut() { state.passthrough = true; } let load_desc = metal::make_load_pass_descriptor_msaa( current_msaa_id, current_target_id, ); if let Some(desc) = load_desc { if let Some(enc) = cmd_buf.render_command_encoder(desc) { encoder = enc; self.setup_encoder(&encoder, current_vw as f32, current_vh as f32); apply_scissor( &encoder, current_scissor, dev(current_vw), dev(current_vh), ); encoder.set_fragment_bytes(¤t_clip, 0); } } } else { // Opacity 1.0 — no intermediate texture needed. layer_stack.push(LayerState { passthrough: true, texture: None, msaa_texture: None, bounds_x: 0.0, bounds_y: 0.0, bounds_w: 0.0, bounds_h: 0.0, opacity: 1.0, parent_target_id: current_target_id, parent_msaa_id: current_msaa_id, parent_vw: current_vw, parent_vh: current_vh, parent_coord_offset: coord_offset, saved_clip_stack: Vec::new(), saved_scissor: current_scissor, saved_clip_shape_stack: Vec::new(), saved_clip_shape: current_clip, }); } }
PaintCommand::PopLayer => { if let Some(state) = layer_stack.pop() { if !state.passthrough { // Flush and end the layer encoder. if !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); } encoder.end_encoding();
// Restore parent state. current_target_id = state.parent_target_id; current_msaa_id = state.parent_msaa_id; current_vw = state.parent_vw; current_vh = state.parent_vh; coord_offset = state.parent_coord_offset; clip_stack = state.saved_clip_stack; current_scissor = state.saved_scissor; clip_shape_stack = state.saved_clip_shape_stack; current_clip = state.saved_clip_shape;
// Resume rendering to the parent target (Load action // preserves the parent's retained MSAA samples). let load_desc = match metal::make_load_pass_descriptor_msaa( current_msaa_id, current_target_id, ) { Some(d) => d, None => continue, }; encoder = match cmd_buf.render_command_encoder(load_desc) { Some(e) => e, None => continue, }; self.setup_encoder(&encoder, current_vw as f32, current_vh as f32); apply_scissor( &encoder, current_scissor, dev(current_vw), dev(current_vh), ); // Rebind the restored rounded clip so the composited // layer quad is clipped to any parent rounded corners. encoder.set_fragment_bytes(¤t_clip, 0);
// Composite the layer texture as a quad with opacity. if let Some(ref tex) = state.texture { let color = [1.0, 1.0, 1.0, state.opacity]; push_layer_quad( &mut batch_vertices, state.bounds_x, state.bounds_y, state.bounds_w, state.bounds_h, color, ); // Flush immediately with the layer texture bound. self.flush_batch_with_texture(&batch_vertices, tex, &encoder); batch_vertices.clear(); } } } }
PaintCommand::FillRect { x, y, width, height, color, } => { let tex_key = TextureKey::Solid; if tex_key != batch_texture && !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); } batch_texture = tex_key; push_solid_quad( &mut batch_vertices, *x + coord_offset.0, *y + coord_offset.1, *width, *height, color_to_f32(color), ); }
PaintCommand::LinearGradient { x, y, width, height, gradient, } => { let tex_key = TextureKey::Solid; if tex_key != batch_texture && !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); } batch_texture = tex_key; push_linear_gradient_quad( &mut batch_vertices, *x + coord_offset.0, *y + coord_offset.1, *width, *height, gradient, ); }
PaintCommand::RadialGradient { x, y, width, height, position_x, position_y, repeat, gradient, } => { let tex_key = TextureKey::Solid; if tex_key != batch_texture && !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); } batch_texture = tex_key; push_radial_gradient_quad( &mut batch_vertices, (*x + coord_offset.0, *y + coord_offset.1, *width, *height), (*position_x, *position_y), *repeat, gradient, ); }
PaintCommand::RoundedRect { x, y, width, height, radii, stroke_width, color, } => { // SDF rounded rects share the solid (no-texture) batch; the // fragment shader branches on the per-vertex SDF mode. let tex_key = TextureKey::Solid; if tex_key != batch_texture && !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); } batch_texture = tex_key; push_sdf_rounded_rect( &mut batch_vertices, *x + coord_offset.0, *y + coord_offset.1, *width, *height, *radii, *stroke_width, color_to_f32(color), ); }
PaintCommand::BoxShadow { x, y, width, height, radii, blur_radius, color, } => { let tex_key = TextureKey::Solid; if tex_key != batch_texture && !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); } batch_texture = tex_key; push_sdf_box_shadow( &mut batch_vertices, *x + coord_offset.0, *y + coord_offset.1, *width, *height, *radii, *blur_radius, color_to_f32(color), ); }
PaintCommand::DrawGlyphs { line, font_size: _, color, } => { // Build glyph quads from the atlas. let text_font = font_resolver.resolve(&line.font_family, line.font_weight, line.italic); let quads = self.atlas.build_text_quads(line, text_font, scale); self.ensure_atlas_textures();
for quad in &quads { let tex_key = TextureKey::AtlasPage(quad.page); if tex_key != batch_texture && !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); } batch_texture = tex_key; push_textured_quad_offset( &mut batch_vertices, quad, color_to_f32(color), coord_offset, ); } }
PaintCommand::DrawImage { x, y, width, height, object_fit, node_id, } => { // Upload image texture if not cached. let nid = node_id.index(); if !self.image_cache.contains(nid) { if let Some(img) = images.get(node_id) { if let Some(tex) = upload_image(self.renderer.device(), img) { self.image_cache.insert(nid, tex); } } }
if self.image_cache.contains(nid) { let Some(img) = images.get(node_id) else { continue; }; let fitted = resolve_object_fit( *object_fit, *x + coord_offset.0, *y + coord_offset.1, *width, *height, img.width as f32, img.height as f32, ); let tex_key = TextureKey::Image(nid); if tex_key != batch_texture && !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); } batch_texture = tex_key; push_image_quad_uv( &mut batch_vertices, fitted.x, fitted.y, fitted.width, fitted.height, fitted.u0, fitted.v0, fitted.u1, fitted.v1, ); } }
PaintCommand::DrawBackgroundImage { x, y, width, height, node_id, size, position_x, position_y, .. } => { let nid = node_id.index(); if !self.image_cache.contains(nid) { if let Some(img) = images.get(node_id) { if let Some(tex) = upload_image(self.renderer.device(), img) { self.image_cache.insert(nid, tex); } } }
if self.image_cache.contains(nid) { let Some(img) = images.get(node_id) else { continue; }; let (draw_w, draw_h) = crate::resolve_background_image_size( *size, *width, *height, img.width as f32, img.height as f32, ); let draw_x = *x + crate::background_position_offset( *position_x, *width - draw_w, *width, ); let draw_y = *y + crate::background_position_offset( *position_y, *height - draw_h, *height, ); let tex_key = TextureKey::Image(nid); if tex_key != batch_texture && !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); } batch_texture = tex_key; push_image_quad( &mut batch_vertices, draw_x + coord_offset.0, draw_y + coord_offset.1, draw_w, draw_h, ); } }
PaintCommand::PushClip { x, y, width, height, radii, } => { // Flush current batch before changing scissor. if !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); }
// Clip rects are in device pixels (scaled from logical). let cx = ((*x + coord_offset.0) * scale).max(0.0) as u32; let cy = ((*y + coord_offset.1) * scale).max(0.0) as u32; let new_clip = ScissorRect { x: cx, y: cy, width: (*width * scale).max(0.0) as u32, height: (*height * scale).max(0.0) as u32, }; current_scissor = current_scissor.intersect(new_clip); clip_stack.push(current_scissor); apply_scissor(&encoder, current_scissor, dev(current_vw), dev(current_vh));
// Rounded clip: a box with non-zero corner radii rounds its // corners off via the fragment-shader SDF; in logical // coordinates (matching `world_pos`). A rectangular clip // inherits the parent's active rounded shape. With a single // clip uniform, the innermost rounded clip wins — adequate for // the common single rounded container case. current_clip = if radii.iter().any(|&r| r > 0.0) { let lx = *x + coord_offset.0; let ly = *y + coord_offset.1; ClipParams::rounded( [lx + *width / 2.0, ly + *height / 2.0], [*width / 2.0, *height / 2.0], *radii, ) } else { *clip_shape_stack.last().unwrap_or(&ClipParams::disabled()) }; clip_shape_stack.push(current_clip); encoder.set_fragment_bytes(¤t_clip, 0); }
PaintCommand::PopClip => { // Flush current batch before changing scissor. if !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); batch_vertices.clear(); }
clip_stack.pop(); current_scissor = clip_stack .last() .copied() .unwrap_or(ScissorRect::full(dev(current_vw), dev(current_vh))); apply_scissor(&encoder, current_scissor, dev(current_vw), dev(current_vh));
clip_shape_stack.pop(); current_clip = clip_shape_stack .last() .copied() .unwrap_or_else(ClipParams::disabled); encoder.set_fragment_bytes(¤t_clip, 0); } } }
// Flush any remaining vertices. if !batch_vertices.is_empty() { self.flush_batch(&batch_vertices, batch_texture, &encoder); }
encoder.end_encoding(); }
/// Set up the render encoder with pipeline state and uniforms. fn setup_encoder( &self, encoder: &RenderCommandEncoder, viewport_width: f32, viewport_height: f32, ) { let uniforms = Uniforms { viewport_size: [viewport_width, viewport_height], }; encoder.set_render_pipeline_state(self.renderer.pipeline()); encoder.set_vertex_bytes(&uniforms, 1); encoder.set_fragment_sampler_state(self.renderer.sampler(), 0); // Always bind a clip uniform so the fragment shader's `buffer(0)` is // defined; a disabled clip is a pass-through. PushClip/PopClip rebind it. encoder.set_fragment_bytes(&ClipParams::disabled(), 0); }
/// Flush a batch of vertices as a single draw call. fn flush_batch( &self, vertices: &[Vertex], texture_key: TextureKey, encoder: &RenderCommandEncoder, ) { if vertices.is_empty() { return; }
// Create vertex buffer. let buffer = match self.renderer.device().new_buffer_with_vertices(vertices) { Some(b) => b, None => return, };
// Bind the appropriate texture. match texture_key { TextureKey::Solid => { encoder.set_fragment_texture(self.renderer.dummy_texture(), 0); } TextureKey::AtlasPage(page) => { if let Some(Some(tex)) = self.atlas_textures.get(page) { encoder.set_fragment_texture(tex, 0); encoder.set_fragment_sampler_state(&self.mask_sampler, 0); } else { encoder.set_fragment_texture(self.renderer.dummy_texture(), 0); } } TextureKey::Image(nid) => { if let Some(tex) = self.image_cache.get(nid) { encoder.set_fragment_texture(tex, 0); encoder.set_fragment_sampler_state(&self.image_sampler, 0); } else { encoder.set_fragment_texture(self.renderer.dummy_texture(), 0); } } }
encoder.set_vertex_buffer(&buffer, 0, 0); encoder.draw_primitives(MTL_PRIMITIVE_TYPE_TRIANGLE, 0, vertices.len() as u64);
// Restore linear sampler after image draws. if matches!(texture_key, TextureKey::Image(_)) { encoder.set_fragment_sampler_state(self.renderer.sampler(), 0); } }
/// Flush vertices as a draw call using a specific texture (for compositing /// layer textures back onto the parent target). fn flush_batch_with_texture( &self, vertices: &[Vertex], texture: &Texture, encoder: &RenderCommandEncoder, ) { if vertices.is_empty() { return; } let buffer = match self.renderer.device().new_buffer_with_vertices(vertices) { Some(b) => b, None => return, }; encoder.set_fragment_texture(texture, 0); encoder.set_fragment_sampler_state(self.renderer.sampler(), 0); encoder.set_vertex_buffer(&buffer, 0, 0); encoder.draw_primitives(MTL_PRIMITIVE_TYPE_TRIANGLE, 0, vertices.len() as u64); }
/// Ensure GPU textures exist for all atlas pages, uploading dirty pages. fn ensure_atlas_textures(&mut self) { let page_count = self.atlas.page_count();
// Grow the texture vec if needed. while self.atlas_textures.len() < page_count { self.atlas_textures.push(None); }
for i in 0..page_count { let needs_upload = self.atlas_textures[i].is_none() || self.atlas.is_page_dirty(i); if needs_upload { if let Some((w, h)) = self.atlas.page_size(i) { if let Some(pixels) = self.atlas.page_pixels(i) { if let Some(tex) = self.renderer.device().new_texture_r8(w, h, pixels) { self.atlas_textures[i] = Some(tex); self.atlas.clear_dirty(i); } } } } } }
/// Clear the image texture cache. Call when navigating to a new page. pub fn clear_image_cache(&mut self) { self.image_cache.clear(); }
/// Ensure the cached on-screen MSAA companion texture matches the given /// device-pixel drawable size, recreating it on size change. The texture is /// the 4×-multisampled rasterization target resolved into the drawable. fn ensure_screen_msaa(&mut self, width: u32, height: u32) { let matches = self .screen_msaa .as_ref() .is_some_and(|t| t.width() == width && t.height() == height); if !matches { self.screen_msaa = self.renderer.device().new_msaa_texture( width, height, self.renderer.sample_count(), ); } }}
// ---------------------------------------------------------------------------// Vertex helpers// ---------------------------------------------------------------------------
/// Push 6 vertices (2 triangles) for a solid-color rectangle.fn push_solid_quad(out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, color: [f32; 4]) { let x0 = x; let y0 = y; let x1 = x + w; let y1 = y + h;
// Triangle 1: TL, TR, BL out.push(Vertex { position: [x0, y0], color, tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_SOLID, ..Vertex::default() }); out.push(Vertex { position: [x1, y0], color, tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_SOLID, ..Vertex::default() }); out.push(Vertex { position: [x0, y1], color, tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_SOLID, ..Vertex::default() });
// Triangle 2: TR, BR, BL out.push(Vertex { position: [x1, y0], color, tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_SOLID, ..Vertex::default() }); out.push(Vertex { position: [x1, y1], color, tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_SOLID, ..Vertex::default() }); out.push(Vertex { position: [x0, y1], color, tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_SOLID, ..Vertex::default() });}
#[derive(Clone, Copy)]struct GradientPoint { x: f32, y: f32,}
#[derive(Clone, Copy)]struct ResolvedGradientStop { color: Color, position: f32,}
const GRADIENT_EPSILON: f32 = 0.0001;
/// Push vertices for a CSS linear-gradient rectangle.////// A single quad with only corner colors is not enough for positioned or/// intermediate color stops: the GPU would interpolate across the whole box and/// smear stops such as `transparent 60%, black`. Instead, clip the rectangle/// into one polygon per stop interval and triangulate each interval. Because/// `t` is affine for the supported keyword directions, interpolating vertex/// colors within one stop interval is exact.fn push_linear_gradient_quad( out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, gradient: &LinearGradient,) { if w <= 0.0 || h <= 0.0 { return; }
let stops = resolve_linear_gradient_stops(gradient); if stops.is_empty() { return; }
let mut cuts = vec![0.0, 1.0]; for stop in &stops { if stop.position > 0.0 && stop.position < 1.0 { cuts.push(stop.position); } } cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); cuts.dedup_by(|a, b| (*a - *b).abs() <= GRADIENT_EPSILON);
let vert = |px: f32, py: f32, color: [f32; 4]| Vertex { position: [px, py], color, tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_SOLID, ..Vertex::default() };
for pair in cuts.windows(2) { let t0 = pair[0]; let t1 = pair[1]; if t1 - t0 <= GRADIENT_EPSILON { continue; } let polygon = clip_gradient_interval(gradient.direction, w, h, t0, t1); if polygon.len() < 3 { continue; }
let p0 = polygon[0]; for i in 1..polygon.len() - 1 { let p1 = polygon[i]; let p2 = polygon[i + 1]; for p in [p0, p1, p2] { let t = gradient_t(gradient.direction, p.x, p.y, w, h); let color = color_to_f32(&gradient_color_at_interval(&stops, t, t0, t1)); out.push(vert(x + p.x * w, y + p.y * h, color)); } } }}
fn push_radial_gradient_quad( out: &mut Vec<Vertex>, rect: (f32, f32, f32, f32), position: (LengthOrAuto, LengthOrAuto), repeat: BackgroundRepeat, gradient: &RadialGradient,) { let (x, y, w, h) = rect; let (position_x, position_y) = position; if w <= 0.0 || h <= 0.0 { return; }
let stops = resolve_radial_gradient_stops(gradient); if stops.is_empty() { return; }
let offset_x = crate::background_position_offset(position_x, 0.0, w); let offset_y = crate::background_position_offset(position_y, 0.0, h); let image_x = x + offset_x; let image_y = y + offset_y; let image_right = image_x + w; let image_bottom = image_y + h;
let (paint_x, paint_y, paint_w, paint_h) = match repeat { BackgroundRepeat::Repeat => (x, y, w, h), BackgroundRepeat::NoRepeat => { let left = x.max(image_x); let top = y.max(image_y); let right = (x + w).min(image_right); let bottom = (y + h).min(image_bottom); if right <= left || bottom <= top { return; } (left, top, right - left, bottom - top) } };
let center_x = image_x + w * 0.5; let center_y = image_y + h * 0.5; let radius = [ (image_x, image_y), (image_right, image_y), (image_right, image_bottom), (image_x, image_bottom), ] .iter() .map(|(px, py)| (*px - center_x).hypot(*py - center_y)) .fold(0.0, f32::max); if radius <= f32::EPSILON { return; }
let cols = ((paint_w / 18.0).ceil() as usize).clamp(8, 96); let rows = ((paint_h / 18.0).ceil() as usize).clamp(8, 96); let vertex = |px: f32, py: f32| { let t = ((px - center_x).hypot(py - center_y) / radius).clamp(0.0, 1.0); Vertex { position: [px, py], color: color_to_f32(&gradient_color_at_resolved(&stops, t)), tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_SOLID, ..Vertex::default() } };
for row in 0..rows { let y0 = paint_y + paint_h * row as f32 / rows as f32; let y1 = paint_y + paint_h * (row + 1) as f32 / rows as f32; for col in 0..cols { let x0 = paint_x + paint_w * col as f32 / cols as f32; let x1 = paint_x + paint_w * (col + 1) as f32 / cols as f32; let top_left = vertex(x0, y0); let top_right = vertex(x1, y0); let bottom_right = vertex(x1, y1); let bottom_left = vertex(x0, y1); out.extend_from_slice(&[ top_left, top_right, bottom_right, top_left, bottom_right, bottom_left, ]); } }}
fn gradient_t(direction: LinearGradientDirection, x: f32, y: f32, width: f32, height: f32) -> f32 { match direction { LinearGradientDirection::ToBottom => y, LinearGradientDirection::ToTop => 1.0 - y, LinearGradientDirection::ToRight => x, LinearGradientDirection::ToLeft => 1.0 - x, LinearGradientDirection::ToBottomRight => { diagonal_gradient_t(x, y, width, height, 1.0, 1.0) } LinearGradientDirection::ToBottomLeft => { diagonal_gradient_t(x, y, width, height, -1.0, 1.0) } LinearGradientDirection::ToTopRight => diagonal_gradient_t(x, y, width, height, 1.0, -1.0), LinearGradientDirection::ToTopLeft => diagonal_gradient_t(x, y, width, height, -1.0, -1.0), }}
fn diagonal_gradient_t(x: f32, y: f32, width: f32, height: f32, dir_x: f32, dir_y: f32) -> f32 { let span = width.abs() * dir_x.abs() + height.abs() * dir_y.abs(); if span <= f32::EPSILON { return 0.0; }
let projection = (x - 0.5) * width * dir_x + (y - 0.5) * height * dir_y; ((projection + span * 0.5) / span).clamp(0.0, 1.0)}
#[cfg(test)]fn gradient_color_at(gradient: &LinearGradient, t: f32) -> Color { gradient_color_at_resolved(&resolve_linear_gradient_stops(gradient), t)}
fn resolve_linear_gradient_stops(gradient: &LinearGradient) -> Vec<ResolvedGradientStop> { resolve_gradient_stop_list(&gradient.stops)}
fn resolve_radial_gradient_stops(gradient: &RadialGradient) -> Vec<ResolvedGradientStop> { resolve_gradient_stop_list(&gradient.stops)}
fn resolve_gradient_stop_list(stops: &[GradientStop]) -> Vec<ResolvedGradientStop> { let len = stops.len(); if len == 0 { return Vec::new(); } if len == 1 { return vec![ResolvedGradientStop { color: stops[0].color, position: stops[0].position.unwrap_or(0.0), }]; }
let mut positions: Vec<Option<f32>> = stops.iter().map(|s| s.position).collect(); if positions[0].is_none() { positions[0] = Some(0.0); } if positions[len - 1].is_none() { positions[len - 1] = Some(1.0); }
let mut i = 0; while i < len { if positions[i].is_some() { i += 1; continue; } let run_start = i; while i < len && positions[i].is_none() { i += 1; } let run_end = i; let prev = positions[run_start - 1].unwrap_or(0.0); let next = positions[run_end].unwrap_or(prev); let count = (run_end - run_start + 1) as f32; for (offset, slot) in positions[run_start..run_end].iter_mut().enumerate() { let step = (offset as f32 + 1.0) / count; *slot = Some(prev + (next - prev) * step); } }
let mut last = f32::NEG_INFINITY; stops .iter() .zip(positions) .map(|(stop, position)| { let mut position = position.unwrap_or(last.max(0.0)); if position < last { position = last; } last = position; ResolvedGradientStop { color: stop.color, position, } }) .collect()}
fn gradient_color_at_resolved(stops: &[ResolvedGradientStop], t: f32) -> Color { let Some(first) = stops.first() else { return Color::new(0, 0, 0, 0); }; if t <= first.position { return first.color; }
for pair in stops.windows(2) { let a = pair[0]; let b = pair[1]; if t <= b.position { let span = b.position - a.position; if span.abs() <= f32::EPSILON { return b.color; } let local = ((t - a.position) / span).clamp(0.0, 1.0); return lerp_color(a.color, b.color, local); } }
stops.last().map(|s| s.color).unwrap_or(first.color)}
fn gradient_color_at_interval( stops: &[ResolvedGradientStop], t: f32, interval_start: f32, interval_end: f32,) -> Color { let t = if (t - interval_start).abs() <= GRADIENT_EPSILON && has_repeated_gradient_stop_at(stops, interval_start) { (t + GRADIENT_EPSILON).min(interval_end) } else { t }; gradient_color_at_resolved(stops, t)}
fn has_repeated_gradient_stop_at(stops: &[ResolvedGradientStop], position: f32) -> bool { stops.windows(2).any(|pair| { (pair[0].position - position).abs() <= GRADIENT_EPSILON && (pair[1].position - position).abs() <= GRADIENT_EPSILON })}
fn clip_gradient_interval( direction: LinearGradientDirection, width: f32, height: f32, min_t: f32, max_t: f32,) -> Vec<GradientPoint> { let polygon = vec![ GradientPoint { x: 0.0, y: 0.0 }, GradientPoint { x: 1.0, y: 0.0 }, GradientPoint { x: 1.0, y: 1.0 }, GradientPoint { x: 0.0, y: 1.0 }, ]; let polygon = clip_gradient_half_plane(polygon, direction, width, height, min_t, true); clip_gradient_half_plane(polygon, direction, width, height, max_t, false)}
fn clip_gradient_half_plane( polygon: Vec<GradientPoint>, direction: LinearGradientDirection, width: f32, height: f32, boundary: f32, keep_greater: bool,) -> Vec<GradientPoint> { if polygon.is_empty() { return polygon; }
let mut out = Vec::new(); let mut prev = *polygon.last().unwrap(); let mut prev_t = gradient_t(direction, prev.x, prev.y, width, height); let mut prev_inside = gradient_inside(prev_t, boundary, keep_greater); for current in polygon { let current_t = gradient_t(direction, current.x, current.y, width, height); let current_inside = gradient_inside(current_t, boundary, keep_greater);
if current_inside != prev_inside { if let Some(intersection) = gradient_boundary_intersection(prev, prev_t, current, current_t, boundary) { out.push(intersection); } } if current_inside { out.push(current); }
prev = current; prev_t = current_t; prev_inside = current_inside; } out}
fn gradient_inside(t: f32, boundary: f32, keep_greater: bool) -> bool { if keep_greater { t >= boundary - GRADIENT_EPSILON } else { t <= boundary + GRADIENT_EPSILON }}
fn gradient_boundary_intersection( a: GradientPoint, a_t: f32, b: GradientPoint, b_t: f32, boundary: f32,) -> Option<GradientPoint> { let denom = b_t - a_t; if denom.abs() <= f32::EPSILON { return None; } let local = ((boundary - a_t) / denom).clamp(0.0, 1.0); Some(GradientPoint { x: a.x + (b.x - a.x) * local, y: a.y + (b.y - a.y) * local, })}
fn lerp_color(a: Color, b: Color, t: f32) -> Color { let alpha_a = a.a as f32 / 255.0; let alpha_b = b.a as f32 / 255.0; let alpha = alpha_a + (alpha_b - alpha_a) * t; let out_alpha = (alpha * 255.0).round().clamp(0.0, 255.0) as u8;
if alpha <= f32::EPSILON { return Color::new(0, 0, 0, 0); }
fn premul_channel(a: u8, alpha_a: f32, b: u8, alpha_b: f32, alpha: f32, t: f32) -> u8 { let premul_a = a as f32 * alpha_a; let premul_b = b as f32 * alpha_b; ((premul_a + (premul_b - premul_a) * t) / alpha) .round() .clamp(0.0, 255.0) as u8 }
Color::new( premul_channel(a.r, alpha_a, b.r, alpha_b, alpha, t), premul_channel(a.g, alpha_a, b.g, alpha_b, alpha, t), premul_channel(a.b, alpha_a, b.b, alpha_b, alpha, t), out_alpha, )}
/// Push 6 vertices (2 triangles) covering a rounded rectangle's bounding box,/// tagged for the fragment shader's signed-distance-field rounded-rect mode./// `radii` is (top-left, top-right, bottom-right, bottom-left). `stroke_width`/// of `0.0` fills the interior; a positive value strokes an inward ring.#[allow(clippy::too_many_arguments)]fn push_sdf_rounded_rect( out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, radii: [f32; 4], stroke_width: f32, color: [f32; 4],) { let x0 = x; let y0 = y; let x1 = x + w; let y1 = y + h; let half = [w * 0.5, h * 0.5]; // Fragment-local position is measured from the rect center, so the SDF is // evaluated about the origin regardless of any layer coordinate offset. let cx = x + half[0]; let cy = y + half[1]; let vert = |px: f32, py: f32| Vertex { position: [px, py], color, tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_SDF, local_pos: [px - cx, py - cy], rect_half: half, corner_radii: radii, stroke_width, };
// Triangle 1: TL, TR, BL out.push(vert(x0, y0)); out.push(vert(x1, y0)); out.push(vert(x0, y1)); // Triangle 2: TR, BR, BL out.push(vert(x1, y0)); out.push(vert(x1, y1)); out.push(vert(x0, y1));}
/// Push 6 vertices for an analytic blurred rounded-rect shadow.////// The input rect is the shadow-casting shape after CSS spread has been/// applied. The generated quad is expanded by the blur radius so the shader has/// room to shade the soft falloff outside the shape.#[allow(clippy::too_many_arguments)]fn push_sdf_box_shadow( out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, radii: [f32; 4], blur_radius: f32, color: [f32; 4],) { if w <= 0.0 || h <= 0.0 { return; }
let blur = blur_radius.max(0.0); let pad = blur + 1.0; let qx0 = x - pad; let qy0 = y - pad; let qx1 = x + w + pad; let qy1 = y + h + pad; let half = [w * 0.5, h * 0.5]; let cx = x + half[0]; let cy = y + half[1]; let vert = |px: f32, py: f32| Vertex { position: [px, py], color, tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_SHADOW, local_pos: [px - cx, py - cy], rect_half: half, corner_radii: radii, stroke_width: blur, };
out.push(vert(qx0, qy0)); out.push(vert(qx1, qy0)); out.push(vert(qx0, qy1)); out.push(vert(qx1, qy0)); out.push(vert(qx1, qy1)); out.push(vert(qx0, qy1));}
/// Push 6 vertices (2 triangles) for a textured glyph quad.#[cfg(test)]fn push_textured_quad(out: &mut Vec<Vertex>, quad: &TexturedQuad, color: [f32; 4]) { push_textured_quad_offset(out, quad, color, (0.0, 0.0));}
/// Push 6 vertices for a textured glyph quad with a coordinate offset.fn push_textured_quad_offset( out: &mut Vec<Vertex>, quad: &TexturedQuad, color: [f32; 4], offset: (f32, f32),) { let x0 = quad.x + offset.0; let y0 = quad.y + offset.1; let x1 = x0 + quad.width; let y1 = y0 + quad.height; let u0 = quad.u0; let v0 = quad.v0; let u1 = quad.u1; let v1 = quad.v1;
// Triangle 1: TL, TR, BL out.push(Vertex { position: [x0, y0], color, tex_coord: [u0, v0], use_texture: TEXTURE_MODE_MASK, ..Vertex::default() }); out.push(Vertex { position: [x1, y0], color, tex_coord: [u1, v0], use_texture: TEXTURE_MODE_MASK, ..Vertex::default() }); out.push(Vertex { position: [x0, y1], color, tex_coord: [u0, v1], use_texture: TEXTURE_MODE_MASK, ..Vertex::default() });
// Triangle 2: TR, BR, BL out.push(Vertex { position: [x1, y0], color, tex_coord: [u1, v0], use_texture: TEXTURE_MODE_MASK, ..Vertex::default() }); out.push(Vertex { position: [x1, y1], color, tex_coord: [u1, v1], use_texture: TEXTURE_MODE_MASK, ..Vertex::default() }); out.push(Vertex { position: [x0, y1], color, tex_coord: [u0, v1], use_texture: TEXTURE_MODE_MASK, ..Vertex::default() });}
/// Push 6 vertices (2 triangles) for a textured image quad.fn push_image_quad(out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32) { push_image_quad_uv(out, x, y, w, h, 0.0, 0.0, 1.0, 1.0);}
#[derive(Debug, Clone, Copy)]struct FittedImageQuad { x: f32, y: f32, width: f32, height: f32, u0: f32, v0: f32, u1: f32, v1: f32,}
fn resolve_object_fit( fit: ObjectFit, x: f32, y: f32, width: f32, height: f32, image_width: f32, image_height: f32,) -> FittedImageQuad { if width <= 0.0 || height <= 0.0 || image_width <= 0.0 || image_height <= 0.0 { return FittedImageQuad { x, y, width: width.max(0.0), height: height.max(0.0), u0: 0.0, v0: 0.0, u1: 1.0, v1: 1.0, }; }
match fit { ObjectFit::Fill => FittedImageQuad { x, y, width, height, u0: 0.0, v0: 0.0, u1: 1.0, v1: 1.0, }, ObjectFit::Contain => { let scale = (width / image_width).min(height / image_height); let draw_w = image_width * scale; let draw_h = image_height * scale; FittedImageQuad { x: x + (width - draw_w) * 0.5, y: y + (height - draw_h) * 0.5, width: draw_w, height: draw_h, u0: 0.0, v0: 0.0, u1: 1.0, v1: 1.0, } } ObjectFit::Cover => { let box_aspect = width / height; let image_aspect = image_width / image_height; if image_aspect > box_aspect { let visible_fraction = (box_aspect / image_aspect).clamp(0.0, 1.0); let crop = (1.0 - visible_fraction) * 0.5; FittedImageQuad { x, y, width, height, u0: crop, v0: 0.0, u1: 1.0 - crop, v1: 1.0, } } else { let visible_fraction = (image_aspect / box_aspect).clamp(0.0, 1.0); let crop = (1.0 - visible_fraction) * 0.5; FittedImageQuad { x, y, width, height, u0: 0.0, v0: crop, u1: 1.0, v1: 1.0 - crop, } } } }}
/// Push 6 vertices (2 triangles) for a textured image quad with explicit UVs.#[allow(clippy::too_many_arguments)]fn push_image_quad_uv( out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, u0: f32, v0: f32, u1: f32, v1: f32,) { let x0 = x; let y0 = y; let x1 = x + w; let y1 = y + h; let color = [1.0, 1.0, 1.0, 1.0]; // no tint
// Triangle 1: TL, TR, BL out.push(Vertex { position: [x0, y0], color, tex_coord: [u0, v0], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() }); out.push(Vertex { position: [x1, y0], color, tex_coord: [u1, v0], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() }); out.push(Vertex { position: [x0, y1], color, tex_coord: [u0, v1], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() });
// Triangle 2: TR, BR, BL out.push(Vertex { position: [x1, y0], color, tex_coord: [u1, v0], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() }); out.push(Vertex { position: [x1, y1], color, tex_coord: [u1, v1], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() }); out.push(Vertex { position: [x0, y1], color, tex_coord: [u0, v1], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() });}
/// Push 6 vertices for compositing a layer texture back onto the parent target.////// The layer texture is drawn as a full quad at the layer's screen position./// The `color` alpha channel carries the layer opacity.fn push_layer_quad(out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, color: [f32; 4]) { let x0 = x; let y0 = y; let x1 = x + w; let y1 = y + h;
// Triangle 1: TL, TR, BL out.push(Vertex { position: [x0, y0], color, tex_coord: [0.0, 0.0], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() }); out.push(Vertex { position: [x1, y0], color, tex_coord: [1.0, 0.0], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() }); out.push(Vertex { position: [x0, y1], color, tex_coord: [0.0, 1.0], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() });
// Triangle 2: TR, BR, BL out.push(Vertex { position: [x1, y0], color, tex_coord: [1.0, 0.0], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() }); out.push(Vertex { position: [x1, y1], color, tex_coord: [1.0, 1.0], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() }); out.push(Vertex { position: [x0, y1], color, tex_coord: [0.0, 1.0], use_texture: TEXTURE_MODE_COLOR, ..Vertex::default() });}
// ---------------------------------------------------------------------------// Helpers// ---------------------------------------------------------------------------
/// Convert a CSS RGBA color to a float array [r, g, b, a] in 0.0–1.0.fn color_to_f32(c: &Color) -> [f32; 4] { [ c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0, c.a as f32 / 255.0, ]}
/// Upload an image to the GPU as an RGBA8 texture.fn upload_image(device: &Device, img: &Image) -> Option<Texture> { if img.width == 0 || img.height == 0 || img.data.is_empty() { return None; }
// Convert RGBA to BGRA for Metal's preferred pixel format. let mut bgra = Vec::with_capacity(img.data.len()); for chunk in img.data.chunks_exact(4) { bgra.push(chunk[2]); // B bgra.push(chunk[1]); // G bgra.push(chunk[0]); // R bgra.push(chunk[3]); // A }
device.new_texture( img.width, img.height, MTL_PIXEL_FORMAT_BGRA8_UNORM, &bgra, img.width * 4, )}
/// Apply a scissor rect to the encoder, clamping to valid dimensions.fn apply_scissor(encoder: &RenderCommandEncoder, rect: ScissorRect, vw: u32, vh: u32) { // Metal requires scissor rect to be within the render target. let x = rect.x.min(vw); let y = rect.y.min(vh); let w = rect.width.min(vw.saturating_sub(x)).max(1); let h = rect.height.min(vh.saturating_sub(y)).max(1); encoder.set_scissor_rect(x as u64, y as u64, w as u64, h as u64);}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
#[cfg(test)]mod tests { use super::*;
#[test] fn scissor_rect_full() { let r = ScissorRect::full(800, 600); assert_eq!(r.x, 0); assert_eq!(r.y, 0); assert_eq!(r.width, 800); assert_eq!(r.height, 600); }
#[test] fn scissor_rect_intersect() { let a = ScissorRect { x: 0, y: 0, width: 100, height: 100, }; let b = ScissorRect { x: 50, y: 50, width: 100, height: 100, }; let c = a.intersect(b); assert_eq!(c.x, 50); assert_eq!(c.y, 50); assert_eq!(c.width, 50); assert_eq!(c.height, 50); }
#[test] fn scissor_rect_no_overlap() { let a = ScissorRect { x: 0, y: 0, width: 50, height: 50, }; let b = ScissorRect { x: 100, y: 100, width: 50, height: 50, }; let c = a.intersect(b); assert_eq!(c.width, 0); assert_eq!(c.height, 0); }
#[test] fn color_conversion() { let c = Color { r: 255, g: 128, b: 0, a: 255, }; let f = color_to_f32(&c); assert!((f[0] - 1.0).abs() < 0.01); assert!((f[1] - 0.502).abs() < 0.01); assert!((f[2] - 0.0).abs() < 0.01); assert!((f[3] - 1.0).abs() < 0.01); }
#[test] fn gradient_color_at_uses_intermediate_stops() { let gradient = LinearGradient { direction: LinearGradientDirection::ToBottom, stops: vec![ we_css::values::GradientStop { color: Color::rgb(255, 0, 0), position: Some(0.0), }, we_css::values::GradientStop { color: Color::rgb(0, 255, 0), position: Some(0.5), }, we_css::values::GradientStop { color: Color::rgb(0, 0, 255), position: Some(1.0), }, ], };
assert_eq!(gradient_color_at(&gradient, 0.0), Color::rgb(255, 0, 0)); assert_eq!(gradient_color_at(&gradient, 0.5), Color::rgb(0, 255, 0)); assert_eq!(gradient_color_at(&gradient, 1.0), Color::rgb(0, 0, 255)); }
#[test] fn gradient_color_at_interpolates_transparency_premultiplied() { let gradient = LinearGradient { direction: LinearGradientDirection::ToBottom, stops: vec![ we_css::values::GradientStop { color: Color::new(100, 50, 200, 128), position: Some(0.0), }, we_css::values::GradientStop { color: Color::new(0, 0, 0, 0), position: Some(1.0), }, ], };
assert_eq!( gradient_color_at(&gradient, 0.5), Color::new(100, 50, 200, 64) ); }
#[test] fn unspecified_gradient_stops_are_evenly_distributed() { let gradient = LinearGradient { direction: LinearGradientDirection::ToBottom, stops: vec![ we_css::values::GradientStop { color: Color::rgb(255, 0, 0), position: None, }, we_css::values::GradientStop { color: Color::rgb(0, 255, 0), position: None, }, we_css::values::GradientStop { color: Color::rgb(0, 0, 255), position: None, }, ], };
let stops = resolve_linear_gradient_stops(&gradient); assert_eq!(stops.len(), 3); assert!((stops[0].position - 0.0).abs() < 0.001); assert!((stops[1].position - 0.5).abs() < 0.001); assert!((stops[2].position - 1.0).abs() < 0.001); }
#[test] fn repeated_gradient_stop_uses_right_color_for_next_interval() { let gradient = LinearGradient { direction: LinearGradientDirection::ToBottom, stops: vec![ we_css::values::GradientStop { color: Color::rgb(255, 0, 0), position: Some(0.0), }, we_css::values::GradientStop { color: Color::rgb(0, 255, 0), position: Some(0.5), }, we_css::values::GradientStop { color: Color::rgb(0, 0, 0), position: Some(0.5), }, we_css::values::GradientStop { color: Color::rgb(255, 255, 255), position: Some(1.0), }, ], };
let stops = resolve_linear_gradient_stops(&gradient); assert_eq!( gradient_color_at_interval(&stops, 0.5, 0.0, 0.5), Color::rgb(0, 255, 0) ); assert_eq!( gradient_color_at_interval(&stops, 0.5, 0.5, 1.0), Color::rgb(0, 0, 0) ); }
#[test] fn diagonal_gradient_coordinates_use_rect_aspect_ratio() { let square_t = gradient_t( LinearGradientDirection::ToBottomRight, 1.0, 0.0, 100.0, 100.0, ); let wide_t = gradient_t( LinearGradientDirection::ToBottomRight, 1.0, 0.0, 300.0, 100.0, ); let tall_t = gradient_t( LinearGradientDirection::ToBottomRight, 1.0, 0.0, 100.0, 300.0, );
assert!((square_t - 0.5).abs() < 0.001); assert!((wide_t - 0.75).abs() < 0.001); assert!((tall_t - 0.25).abs() < 0.001); }
#[test] fn diagonal_gradient_interval_clip_uses_rect_aspect_ratio() { let square = clip_gradient_interval( LinearGradientDirection::ToBottomRight, 100.0, 100.0, 0.5, 1.0, ); let wide = clip_gradient_interval( LinearGradientDirection::ToBottomRight, 300.0, 100.0, 0.75, 1.0, );
assert!(square .iter() .any(|p| (p.x - 1.0).abs() < 0.001 && (p.y - 0.0).abs() < 0.001)); assert!(wide .iter() .any(|p| (p.x - 1.0).abs() < 0.001 && (p.y - 0.0).abs() < 0.001)); }
#[test] fn positioned_gradient_stop_splits_quad() { let gradient = LinearGradient { direction: LinearGradientDirection::ToBottom, stops: vec![ we_css::values::GradientStop { color: Color::new(0, 0, 0, 0), position: Some(0.6), }, we_css::values::GradientStop { color: Color::new(0, 0, 0, 255), position: Some(1.0), }, ], }; let mut verts = Vec::new(); push_linear_gradient_quad(&mut verts, 0.0, 0.0, 100.0, 100.0, &gradient);
assert_eq!(verts.len(), 12); assert!( verts.iter().any(|v| (v.position[1] - 60.0).abs() < 0.01), "expected a split at the 60% stop" ); assert!(verts .iter() .filter(|v| v.position[1] <= 60.01) .all(|v| v.color[3].abs() < 0.01)); assert!(verts .iter() .any(|v| (v.position[1] - 100.0).abs() < 0.01 && v.color[3] > 0.99)); }
#[test] fn solid_quad_vertex_count() { let mut verts = Vec::new(); push_solid_quad(&mut verts, 0.0, 0.0, 100.0, 50.0, [1.0, 0.0, 0.0, 1.0]); assert_eq!(verts.len(), 6); // Solid quads do not sample a texture. for v in &verts { assert_eq!(v.use_texture, TEXTURE_MODE_SOLID); } }
#[test] fn sdf_box_shadow_vertices_expand_for_blur() { let mut verts = Vec::new(); push_sdf_box_shadow( &mut verts, 10.0, 20.0, 100.0, 50.0, [8.0; 4], 12.0, [0.0, 0.0, 0.0, 0.5], );
assert_eq!(verts.len(), 6); assert!(verts.iter().all(|v| v.use_texture == TEXTURE_MODE_SHADOW)); assert!(verts.iter().all(|v| v.stroke_width == 12.0)); assert!(verts.iter().any(|v| v.position == [-3.0, 7.0])); assert!(verts.iter().any(|v| v.position == [123.0, 83.0])); assert!(verts.iter().all(|v| v.rect_half == [50.0, 25.0])); }
fn test_radial_gradient() -> RadialGradient { RadialGradient { stops: vec![ GradientStop { color: Color::new(87, 41, 255, 59), position: Some(0.0), }, GradientStop { color: Color::new(0, 0, 0, 0), position: Some(100.0), }, ], } }
#[test] fn shifted_no_repeat_radial_gradient_clips_to_positioned_image() { let mut verts = Vec::new(); push_radial_gradient_quad( &mut verts, (0.0, 0.0, 100.0, 50.0), (LengthOrAuto::Length(60.0), LengthOrAuto::Length(0.0)), BackgroundRepeat::NoRepeat, &test_radial_gradient(), );
assert!(!verts.is_empty()); let min_x = verts .iter() .map(|v| v.position[0]) .fold(f32::INFINITY, f32::min); let max_x = verts .iter() .map(|v| v.position[0]) .fold(f32::NEG_INFINITY, f32::max); assert!((min_x - 60.0).abs() < 0.001); assert!((max_x - 100.0).abs() < 0.001); }
#[test] fn shifted_repeating_radial_gradient_keeps_full_paint_area() { let mut verts = Vec::new(); push_radial_gradient_quad( &mut verts, (0.0, 0.0, 100.0, 50.0), (LengthOrAuto::Length(60.0), LengthOrAuto::Length(0.0)), BackgroundRepeat::Repeat, &test_radial_gradient(), );
assert!(!verts.is_empty()); let min_x = verts .iter() .map(|v| v.position[0]) .fold(f32::INFINITY, f32::min); let max_x = verts .iter() .map(|v| v.position[0]) .fold(f32::NEG_INFINITY, f32::max); assert!((min_x - 0.0).abs() < 0.001); assert!((max_x - 100.0).abs() < 0.001); }
#[test] fn textured_quad_vertex_count() { let quad = TexturedQuad { x: 10.0, y: 20.0, width: 8.0, height: 12.0, u0: 0.0, v0: 0.0, u1: 0.5, v1: 0.5, color: [1.0, 1.0, 1.0, 1.0], page: 0, }; let mut verts = Vec::new(); push_textured_quad(&mut verts, &quad, [1.0, 1.0, 1.0, 1.0]); assert_eq!(verts.len(), 6); // Glyph atlas quads sample R8 masks, not color textures. for v in &verts { assert_eq!(v.use_texture, TEXTURE_MODE_MASK); } }
#[test] fn image_quad_vertex_count() { let mut verts = Vec::new(); push_image_quad(&mut verts, 0.0, 0.0, 200.0, 150.0); assert_eq!(verts.len(), 6); for v in &verts { assert_eq!(v.use_texture, TEXTURE_MODE_COLOR); } // Check UV coordinates cover full image (0,0)→(1,1) assert_eq!(verts[0].tex_coord, [0.0, 0.0]); // TL assert_eq!(verts[1].tex_coord, [1.0, 0.0]); // TR assert_eq!(verts[4].tex_coord, [1.0, 1.0]); // BR }
#[test] fn object_fit_cover_crops_wide_source_horizontally() { let fitted = resolve_object_fit(ObjectFit::Cover, 0.0, 0.0, 100.0, 100.0, 200.0, 100.0);
assert_eq!(fitted.x, 0.0); assert_eq!(fitted.y, 0.0); assert_eq!(fitted.width, 100.0); assert_eq!(fitted.height, 100.0); assert!((fitted.u0 - 0.25).abs() < 0.001); assert!((fitted.u1 - 0.75).abs() < 0.001); assert_eq!(fitted.v0, 0.0); assert_eq!(fitted.v1, 1.0); }
#[test] fn object_fit_contain_centers_letterboxed_image() { let fitted = resolve_object_fit(ObjectFit::Contain, 10.0, 20.0, 100.0, 100.0, 200.0, 100.0);
assert_eq!(fitted.x, 10.0); assert_eq!(fitted.width, 100.0); assert!((fitted.y - 45.0).abs() < 0.001); assert!((fitted.height - 50.0).abs() < 0.001); assert_eq!(fitted.u0, 0.0); assert_eq!(fitted.v0, 0.0); assert_eq!(fitted.u1, 1.0); assert_eq!(fitted.v1, 1.0); }
#[test] fn texture_key_equality() { assert_eq!(TextureKey::Solid, TextureKey::Solid); assert_eq!(TextureKey::AtlasPage(0), TextureKey::AtlasPage(0)); assert_ne!(TextureKey::AtlasPage(0), TextureKey::AtlasPage(1)); assert_ne!(TextureKey::Solid, TextureKey::AtlasPage(0)); assert_ne!(TextureKey::Solid, TextureKey::Image(0)); }
#[test] fn layer_quad_vertex_count() { let mut verts = Vec::new(); push_layer_quad(&mut verts, 10.0, 20.0, 100.0, 50.0, [1.0, 1.0, 1.0, 0.5]); assert_eq!(verts.len(), 6); for v in &verts { assert_eq!(v.use_texture, TEXTURE_MODE_COLOR); assert_eq!(v.color[3], 0.5); } assert_eq!(verts[0].tex_coord, [0.0, 0.0]); // TL assert_eq!(verts[1].tex_coord, [1.0, 0.0]); // TR assert_eq!(verts[4].tex_coord, [1.0, 1.0]); // BR }
#[test] fn textured_quad_with_offset() { let quad = TexturedQuad { x: 100.0, y: 200.0, width: 10.0, height: 14.0, u0: 0.0, v0: 0.0, u1: 1.0, v1: 1.0, color: [1.0, 1.0, 1.0, 1.0], page: 0, }; let mut verts = Vec::new(); push_textured_quad_offset(&mut verts, &quad, [1.0, 1.0, 1.0, 1.0], (-50.0, -100.0)); assert_eq!(verts.len(), 6); // TL position should be offset assert_eq!(verts[0].position, [50.0, 100.0]); // BR position assert_eq!(verts[4].position, [60.0, 114.0]); }}