Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990//! Display list construction and the Metal GPU compositor.//!//! Walks a layout tree, generates paint commands ([`PaintCommand`]), and feeds//! them to the Metal compositor ([`GpuRenderer`]) — the single rendering//! backend. On-screen it targets a drawable; headless callers (the e2e harness,//! iframe snapshots) use [`GpuRenderer::render_to_pixels`] to render offscreen//! and read the result back as a BGRA buffer, so tests exercise the exact//! production renderer.
pub mod atlas;pub mod gpu;pub mod layer;
pub use gpu::{GpuRenderer, RenderFonts};
use std::collections::HashMap;
use we_css::values::{Color, LinearGradient, RadialGradient};use we_dom::NodeId;use we_layout::{ BoxType, FormControlInfo, FormControlType, LayoutBox, LayoutTree, Rect, SelectOption, TextLine, SCROLLBAR_WIDTH,};use we_style::computed::{ Appearance, BackgroundRepeat, BackgroundSize, BorderStyle, BoxShadow, Display, LengthOrAuto, ObjectFit, Overflow, Position, TextDecoration, Visibility, WillChange,};
/// Scroll state: maps NodeId of scrollable boxes to their (scroll_x, scroll_y) offsets.pub type ScrollState = HashMap<NodeId, (f32, f32)>;
/// Scroll bar track color (light gray).const SCROLLBAR_TRACK_COLOR: Color = Color { r: 230, g: 230, b: 230, a: 255,};
/// Scroll bar thumb color (darker gray).const SCROLLBAR_THUMB_COLOR: Color = Color { r: 160, g: 160, b: 160, a: 255,};
/// A paint command in the display list.#[derive(Debug)]pub enum PaintCommand { /// Fill a rectangle with a solid color. FillRect { x: f32, y: f32, width: f32, height: f32, color: Color, }, /// Fill or stroke a rounded rectangle with per-corner radii (top-left, /// top-right, bottom-right, bottom-left), evaluated analytically by the /// renderer as a signed-distance field for resolution-independent, /// antialiased corners. `stroke_width == 0.0` fills the interior; a /// positive value strokes a ring of that width inward from the outer edge. RoundedRect { x: f32, y: f32, width: f32, height: f32, radii: [f32; 4], stroke_width: f32, color: Color, }, /// Draw an outer blurred rounded-rectangle shadow. The geometry describes /// the shadow-casting box after spread has been applied; the renderer /// expands the draw quad by `blur_radius` and shades the soft alpha falloff. BoxShadow { x: f32, y: f32, width: f32, height: f32, radii: [f32; 4], blur_radius: f32, color: Color, }, /// Fill a rectangle with a CSS linear gradient. LinearGradient { x: f32, y: f32, width: f32, height: f32, gradient: LinearGradient, }, /// Fill a rectangle with a CSS radial gradient. RadialGradient { x: f32, y: f32, width: f32, height: f32, position_x: LengthOrAuto, position_y: LengthOrAuto, repeat: BackgroundRepeat, gradient: RadialGradient, }, /// Draw a text fragment at a position with styling. DrawGlyphs { line: TextLine, font_size: f32, color: Color, }, /// Draw an image at a position with given display dimensions. DrawImage { x: f32, y: f32, width: f32, height: f32, object_fit: ObjectFit, node_id: NodeId, }, /// Draw a CSS background image inside an element background painting area. DrawBackgroundImage { x: f32, y: f32, width: f32, height: f32, node_id: NodeId, size: BackgroundSize, position_x: LengthOrAuto, position_y: LengthOrAuto, repeat: BackgroundRepeat, }, /// Push a clip rectangle onto the clip stack. All subsequent paint /// commands are clipped to the intersection of all active clip rects. /// /// `radii` are the per-corner radii (top-left, top-right, bottom-right, /// bottom-left) of the clip's rounded corners, in the same logical units as /// the rect. All-zero radii are a plain rectangular clip; non-zero radii /// additionally round the corners off (the renderer keeps the rectangular /// scissor for the straight edges and applies an analytic SDF for the /// corners). PushClip { x: f32, y: f32, width: f32, height: f32, radii: [f32; 4], }, /// Pop the most recent clip rectangle off the clip stack. PopClip, /// Begin a compositing layer. Content within the layer is rendered to an /// intermediate surface and then composited with the given opacity. PushLayer { opacity: f32, x: f32, y: f32, width: f32, height: f32, }, /// End the current compositing layer and composite it onto the parent. PopLayer,}
/// A flat list of paint commands in painter's order.pub type DisplayList = Vec<PaintCommand>;
/// Pending dropdown overlay collected during the main paint pass.struct PendingDropdown { /// Screen position of the select control. x: f32, y: f32, width: f32, height: f32, font_size: f32, options: Vec<SelectOption>, highlighted_index: Option<usize>,}
/// Build a display list from a layout tree.////// Walks the tree in depth-first pre-order (painter's order):/// backgrounds first, then borders, then text on top.pub fn build_display_list(tree: &LayoutTree) -> DisplayList { build_display_list_with_scroll(tree, &HashMap::new())}
/// Build a display list from a layout tree with scroll state.////// `page_scroll` is the viewport-level scroll offset (x, y) applied to all content./// `scroll_state` maps element NodeIds to their per-element scroll offsets.pub fn build_display_list_with_scroll( tree: &LayoutTree, scroll_state: &ScrollState,) -> DisplayList { let mut list = DisplayList::new(); let mut dropdowns = Vec::new(); paint_box( &tree.root, &mut list, (0.0, 0.0), scroll_state, 0.0, &mut dropdowns, ); for dd in &dropdowns { paint_dropdown_overlay(dd, &mut list); } list}
/// Build a display list with page-level scrolling.////// `page_scroll_y` shifts all content vertically (viewport-level scroll).pub fn build_display_list_with_page_scroll( tree: &LayoutTree, page_scroll_y: f32, scroll_state: &ScrollState,) -> DisplayList { let mut list = DisplayList::new(); let mut dropdowns = Vec::new(); paint_box( &tree.root, &mut list, (0.0, -page_scroll_y), scroll_state, 0.0, &mut dropdowns, ); for dd in &dropdowns { paint_dropdown_overlay(dd, &mut list); } list}
/// Build a display list into an existing buffer (clear + refill).////// Reuses the buffer's allocated capacity, avoiding repeated allocation/// when the display list size is stable across frames.pub fn build_display_list_into( tree: &LayoutTree, page_scroll_y: f32, scroll_state: &ScrollState, list: &mut DisplayList,) { list.clear(); let mut dropdowns = Vec::new(); paint_box( &tree.root, list, (0.0, -page_scroll_y), scroll_state, 0.0, &mut dropdowns, ); for dd in &dropdowns { paint_dropdown_overlay(dd, list); }}
/// The background color propagated to the viewport canvas.////// CSS background propagation (CSS2 §14.2): for an HTML document the root/// (`<html>`) element's background paints the whole viewport canvas; if the/// root background is transparent, the `<body>` element's background is used/// instead. Real browsers therefore paint the entire viewport — including the/// area outside a centered or narrower body box, and any area below a short/// document — in that color, rather than leaving it the default white.////// Returns `None` when neither the root nor the body has a non-transparent/// background, in which case callers keep their default canvas color.pub fn canvas_background_color(tree: &LayoutTree) -> Option<Color> { let root = &tree.root; if root.background_color.a > 0 { return Some(root.background_color); } // Root background is transparent: fall back to the body element's. The // body is the first element-level child of the root (`<head>` produces no // box, so the first child box that comes from an element is `<body>`). let body = root .children .iter() .find(|c| matches!(c.box_type, BoxType::Block(_) | BoxType::Inline(_)))?; if body.background_color.a > 0 { Some(body.background_color) } else { None }}
/// Compute the dropdown menu geometry for an open single-select.////// Given the select control's screen-space border box and its options,/// returns `(menu_x, menu_y, menu_width, menu_height, line_height)`.pub fn dropdown_menu_geometry( select_x: f32, select_y: f32, select_w: f32, select_h: f32, font_size: f32, option_count: usize,) -> (f32, f32, f32, f32, f32) { let line_height = font_size * 1.2; let padding = 4.0f32; let border = 1.0f32; let menu_height = option_count as f32 * line_height + padding * 2.0 + border * 2.0; let menu_width = select_w.max(100.0); let menu_x = select_x; let menu_y = select_y + select_h; (menu_x, menu_y, menu_width, menu_height, line_height)}
/// Returns `true` if a box is positioned (absolute or fixed).fn is_positioned(b: &LayoutBox) -> bool { b.position == Position::Absolute || b.position == Position::Fixed}
/// Returns `true` if a layout box requires its own compositing layer.fn needs_compositing_layer(b: &LayoutBox) -> bool { // opacity < 1.0 requires group compositing to render correctly b.opacity < 1.0 // will-change hints signal the compositor to promote to a layer || b.will_change != WillChange::default() // fixed-position elements benefit from their own layer || b.position == Position::Fixed // scrollable containers with overflow render content to a layer texture || ((b.overflow == Overflow::Scroll || b.overflow == Overflow::Auto) && b.content_height > b.rect.height)}
fn paint_box( layout_box: &LayoutBox, list: &mut DisplayList, translate: (f32, f32), scroll_state: &ScrollState, sticky_ref_screen_y: f32, dropdowns: &mut Vec<PendingDropdown>,) { if layout_box.transform.paints_as_zero_scale() { return; }
let visible = layout_box.visibility == Visibility::Visible; let transform_ref = border_box(layout_box); let (transform_tx, transform_ty) = layout_box .transform .paint_offset(transform_ref.width, transform_ref.height); let tx = translate.0 + transform_tx; let ty = translate.1 + transform_ty;
// If this box needs a compositing layer, wrap all its paint commands. let layer = needs_compositing_layer(layout_box); if layer { let bb = border_box(layout_box); list.push(PaintCommand::PushLayer { opacity: layout_box.opacity, x: bb.x + tx, y: bb.y + ty, width: bb.width, height: bb.height, }); }
if visible { if let Some(ref fc) = layout_box.form_control { // Form controls paint their own background, borders, and content. paint_box_shadows(layout_box, list, tx, ty); paint_form_control(layout_box, fc, list, tx, ty); // Collect open dropdown overlays for painting after the main pass. if fc.control_type == FormControlType::Select && fc.dropdown_open { let bb = border_box(layout_box); dropdowns.push(PendingDropdown { x: bb.x + tx, y: bb.y + ty, width: bb.width, height: bb.height, font_size: layout_box.font_size, options: fc.options.clone(), highlighted_index: fc.highlighted_index, }); } } else { // Non-replaced inline boxes (`<a>`, `<span>`) now carry a real // bounding rect so hit-testing can resolve inline links, but their // backgrounds and text-decoration are painted per text fragment in // `paint_text`. Painting a box-level background/border here would // double-paint (and fill the rectangular hull of a wrapped inline), // so skip it for them — matching the prior behaviour when these // boxes had a zero rect. let inline_non_replaced = matches!( layout_box.box_type, BoxType::Inline(_) | BoxType::TextRun { .. } ) && layout_box.replaced_size.is_none() && layout_box.display != Display::InlineBlock; if !inline_non_replaced { paint_box_shadows(layout_box, list, tx, ty); paint_background(layout_box, list, tx, ty); paint_borders(layout_box, list, tx, ty); }
// Emit image paint command for replaced elements using their used // content-box dimensions. Intrinsic dimensions still matter for // layout and for object-fit cropping, but CSS width/height set the // painted destination box. if layout_box.replaced_size.is_some() || layout_box.replaced_ratio.is_some() { if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { list.push(PaintCommand::DrawImage { x: layout_box.rect.x + tx, y: layout_box.rect.y + ty, width: layout_box.rect.width, height: layout_box.rect.height, object_fit: layout_box.object_fit, node_id, }); } } } }
// Determine if this box is scrollable. let scrollable = layout_box.overflow == Overflow::Scroll || layout_box.overflow == Overflow::Auto;
// If this box has overflow clipping, push a clip rect for the padding box. // The clip applies to the box's own inline content (text, list markers) and // to its descendants — backgrounds, borders, and replaced content above // already painted outside the clip per CSS. let clips = layout_box.overflow != Overflow::Visible; if clips { let clip = padding_box(layout_box); let radii = padding_box_radii(layout_box, clip.width, clip.height); list.push(PaintCommand::PushClip { x: clip.x + tx, y: clip.y + ty, width: clip.width, height: clip.height, radii, }); }
if visible && layout_box.form_control.is_none() { paint_text(layout_box, list, tx, ty); paint_list_marker(layout_box, list, tx, ty); }
// Compute child translate: adds scroll offset for scrollable boxes. let mut child_translate = (tx, ty); // When entering a scroll container, update the sticky reference point // to the container's padding box top in screen coordinates (pre-scroll). let mut child_sticky_ref = sticky_ref_screen_y; if scrollable { // The scroll container's padding box top on screen (before scroll). child_sticky_ref = (layout_box.rect.y - layout_box.padding.top) + ty; if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { if let Some(&(sx, sy)) = scroll_state.get(&node_id) { child_translate.0 -= sx; child_translate.1 -= sy; } } }
// Check if any children are positioned (absolute/fixed). let has_positioned = layout_box.children.iter().any(is_positioned);
if has_positioned { // CSS stacking context ordering: // 1. Positioned children with negative z-index // 2. In-flow (non-positioned) children in tree order // 3. Positioned children with z-index >= 0 (or auto) in z-index order
// Collect positioned children indices, partitioned by z-index sign. let mut negative_z: Vec<usize> = Vec::new(); let mut non_negative_z: Vec<usize> = Vec::new();
for (i, child) in layout_box.children.iter().enumerate() { if is_positioned(child) { let z = child.z_index.unwrap_or(0); if z < 0 { negative_z.push(i); } else { non_negative_z.push(i); } } }
// Sort by z-index (stable sort preserves tree order for equal z-index). negative_z.sort_by_key(|&i| layout_box.children[i].z_index.unwrap_or(0)); non_negative_z.sort_by_key(|&i| layout_box.children[i].z_index.unwrap_or(0));
// Paint negative z-index positioned children. for &i in &negative_z { paint_child( &layout_box.children[i], list, child_translate, scroll_state, child_sticky_ref, dropdowns, ); }
// Paint in-flow children in tree order. for child in &layout_box.children { if !is_positioned(child) { paint_child( child, list, child_translate, scroll_state, child_sticky_ref, dropdowns, ); } }
// Paint non-negative z-index positioned children. for &i in &non_negative_z { paint_child( &layout_box.children[i], list, child_translate, scroll_state, child_sticky_ref, dropdowns, ); } } else { // No positioned children — paint all in tree order. for child in &layout_box.children { paint_child( child, list, child_translate, scroll_state, child_sticky_ref, dropdowns, ); } }
if clips { list.push(PaintCommand::PopClip); }
// Paint scroll bars after PopClip so they're not clipped by the container. if scrollable && visible { paint_scrollbars(layout_box, list, tx, ty, scroll_state); }
if layer { list.push(PaintCommand::PopLayer); }}
/// Paint a child box, applying sticky positioning offset when needed.fn paint_child( child: &LayoutBox, list: &mut DisplayList, child_translate: (f32, f32), scroll_state: &ScrollState, sticky_ref_screen_y: f32, dropdowns: &mut Vec<PendingDropdown>,) { if child.position == Position::Sticky { let adjusted = compute_sticky_translate(child, child_translate, sticky_ref_screen_y); paint_box( child, list, adjusted, scroll_state, sticky_ref_screen_y, dropdowns, ); } else { paint_box( child, list, child_translate, scroll_state, sticky_ref_screen_y, dropdowns, ); }}
/// Resolve a `LengthOrAuto` to an optional pixel value for sticky offsets.fn resolve_sticky_px(value: LengthOrAuto, reference: f32) -> Option<f32> { match value { LengthOrAuto::Length(v) => Some(v), LengthOrAuto::Percentage(p) => Some(p / 100.0 * reference), LengthOrAuto::Calc(px, pct) => Some(px + pct / 100.0 * reference), LengthOrAuto::ClampCalc { min_px, px_offset, pct, max_px, } => Some( (px_offset + pct / 100.0 * reference) .max(min_px) .min(max_px), ), LengthOrAuto::FitContent | LengthOrAuto::Auto => None, }}
/// Compute the adjusted translate for a `position: sticky` element.////// The element is clamped so that its margin box stays within its/// `sticky_constraint` rectangle while honouring the CSS offset thresholds/// (`top`, `bottom`, `left`, `right`).fn compute_sticky_translate( child: &LayoutBox, child_translate: (f32, f32), sticky_ref_screen_y: f32,) -> (f32, f32) { let constraint = match child.sticky_constraint { Some(c) => c, None => return child_translate, };
let [css_top, _css_right, css_bottom, _css_left] = child.css_offsets;
let mut delta_y = 0.0f32;
let margin_top_doc = child.rect.y - child.padding.top - child.border.top - child.margin.top; let margin_bottom_doc = child.rect.y + child.rect.height + child.padding.bottom + child.border.bottom + child.margin.bottom; let margin_top_screen = margin_top_doc + child_translate.1; let margin_bottom_screen = margin_bottom_doc + child_translate.1; let constraint_top_screen = constraint.y + child_translate.1; let constraint_bottom_screen = (constraint.y + constraint.height) + child_translate.1;
// Handle `top` stickiness: push the element down so its margin box top // is at least `sticky_ref_screen_y + top`. if let Some(top) = resolve_sticky_px(css_top, constraint.height) { let target = sticky_ref_screen_y + top; let raw_delta = (target - margin_top_screen).max(0.0); // Clamp so margin box bottom does not exceed constraint bottom. let max_delta = (constraint_bottom_screen - margin_bottom_screen).max(0.0); delta_y = raw_delta.min(max_delta); }
// Handle `bottom` stickiness: pull the element up so its margin box // bottom does not go below `sticky_ref_screen_y + visible_height - bottom`. // Without a reliable visible-height at this point we approximate by clamping // against the constraint top. if let Some(bottom) = resolve_sticky_px(css_bottom, constraint.height) { // Bottom stickiness: the element should not scroll below the // visible area minus the bottom offset. The visible bottom is // approximated as constraint_bottom_screen. let target_bottom = constraint_bottom_screen - bottom; let raw = (margin_bottom_screen + delta_y - target_bottom).max(0.0); // Clamp so margin top does not go above constraint top. let max_up = (margin_top_screen + delta_y - constraint_top_screen).max(0.0); delta_y -= raw.min(max_up); }
(child_translate.0, child_translate.1 + delta_y)}
/// Compute the padding box rectangle for a layout box./// The padding box is the content area expanded by padding.fn padding_box(layout_box: &LayoutBox) -> Rect { Rect { x: layout_box.rect.x - layout_box.padding.left, y: layout_box.rect.y - layout_box.padding.top, width: layout_box.rect.width + layout_box.padding.left + layout_box.padding.right, height: layout_box.rect.height + layout_box.padding.top + layout_box.padding.bottom, }}
/// Compute the border box of a layout box (padding box + border widths).fn border_box(layout_box: &LayoutBox) -> Rect { let pb = padding_box(layout_box); Rect { x: pb.x - layout_box.border.left, y: pb.y - layout_box.border.top, width: pb.width + layout_box.border.left + layout_box.border.right, height: pb.height + layout_box.border.top + layout_box.border.bottom, }}
/// Extract the NodeId from a BoxType, if it has one.pub(crate) fn node_id_from_box_type(box_type: &BoxType) -> Option<NodeId> { match box_type { BoxType::Block(id) | BoxType::Inline(id) => Some(*id), BoxType::TextRun { node, .. } => Some(*node), BoxType::Anonymous => None, }}
/// Whether `layout_box` has any non-zero corner radius.fn has_border_radius(layout_box: &LayoutBox) -> bool { layout_box.border_radii.iter().any(|&r| r > 0.0)}
/// If the box has a uniform border (all four sides the same non-zero width,/// the same color, and a solid style), return that `(width, color)`. Rounded/// borders are only painted as a clean ring in this uniform case; non-uniform/// rounded borders fall back to the square per-side painter.fn uniform_solid_border(layout_box: &LayoutBox) -> Option<(f32, Color)> { let b = &layout_box.border; let w = b.top; if w <= 0.0 || b.right != w || b.bottom != w || b.left != w { return None; } let styles = &layout_box.border_styles; if styles.iter().any(|s| *s != BorderStyle::Solid) { return None; } let colors = &layout_box.border_colors; if colors.iter().any(|c| *c != colors[0]) { return None; } Some((w, colors[0]))}
/// Compute the per-corner radii of a box's padding box for overflow clipping.////// The border box carries `border_radii`; the padding box (the overflow clip/// rect) sits inside the border, so each corner radius shrinks by the adjacent/// border widths. Per CSS, the inner radius of a corner is the outer radius/// minus the border width on that side, floored at zero. Because the renderer/// uses a single (circular) radius per corner, we subtract the larger of the/// two borders meeting at the corner so content never bleeds past the curve./// The result is clamped so opposing corners cannot overlap the padding box.fn padding_box_radii(layout_box: &LayoutBox, pad_w: f32, pad_h: f32) -> [f32; 4] { if !has_border_radius(layout_box) { return [0.0; 4]; } let r = layout_box.border_radii; let b = &layout_box.border; let inner = [ (r[0] - b.left.max(b.top)).max(0.0), // top-left (r[1] - b.right.max(b.top)).max(0.0), // top-right (r[2] - b.right.max(b.bottom)).max(0.0), // bottom-right (r[3] - b.left.max(b.bottom)).max(0.0), // bottom-left ]; clamp_radii(pad_w, pad_h, inner)}
/// Clamp the four corner radii so opposing corners never overlap the box.fn clamp_radii(w: f32, h: f32, radii: [f32; 4]) -> [f32; 4] { let max_r = (w / 2.0).min(h / 2.0).max(0.0); [ radii[0].clamp(0.0, max_r), radii[1].clamp(0.0, max_r), radii[2].clamp(0.0, max_r), radii[3].clamp(0.0, max_r), ]}
/// Fill a rounded rectangle with per-corner radii (top-left, top-right,/// bottom-right, bottom-left). Emitted as a [`PaintCommand::RoundedRect`] that/// the renderer evaluates as an analytic signed-distance field, giving/// mathematically perfect, resolution-independent antialiased corners. All-zero/// radii collapse to a plain `FillRect`.fn fill_rounded_rect( list: &mut DisplayList, x: f32, y: f32, w: f32, h: f32, radii: [f32; 4], color: Color,) { if w <= 0.0 || h <= 0.0 || color.a == 0 { return; } let radii = clamp_radii(w, h, radii); if radii.iter().all(|&r| r <= 0.0) { list.push(PaintCommand::FillRect { x, y, width: w, height: h, color, }); return; } list.push(PaintCommand::RoundedRect { x, y, width: w, height: h, radii, stroke_width: 0.0, color, });}
/// Stroke a rounded-rectangle outline of the given `thickness` (drawn inward/// from the outer edge). Emitted as a [`PaintCommand::RoundedRect`] with a/// non-zero stroke width; the renderer evaluates the analytic SDF annulus. Used/// for rounded borders and focus rings without needing the backdrop color.#[allow(clippy::too_many_arguments)]fn stroke_rounded_rect( list: &mut DisplayList, x: f32, y: f32, w: f32, h: f32, radii: [f32; 4], thickness: f32, color: Color,) { if w <= 0.0 || h <= 0.0 || thickness <= 0.0 || color.a == 0 { return; } let radii = clamp_radii(w, h, radii); let t = thickness.min(w / 2.0).min(h / 2.0); list.push(PaintCommand::RoundedRect { x, y, width: w, height: h, radii, stroke_width: t, color, });}
fn paint_box_shadows(layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: f32) { if layout_box.box_shadows.is_empty() { return; } let bb = border_box(layout_box); if bb.width <= 0.0 || bb.height <= 0.0 { return; } for shadow in &layout_box.box_shadows { paint_box_shadow(layout_box, shadow, bb, list, (tx, ty)); }}
fn paint_box_shadow( layout_box: &LayoutBox, shadow: &BoxShadow, border_box: Rect, list: &mut DisplayList, translate: (f32, f32),) { if shadow.color.a == 0 { return; }
let blur = shadow.blur_radius.max(0.0); if blur <= 0.5 { paint_shadow_layer(layout_box, shadow, border_box, list, translate, 0.0, 1.0); return; }
paint_blurred_shadow_layer(layout_box, shadow, border_box, list, translate, blur);}
fn paint_blurred_shadow_layer( layout_box: &LayoutBox, shadow: &BoxShadow, border_box: Rect, list: &mut DisplayList, translate: (f32, f32), blur: f32,) { let spread = shadow.spread_radius; let x = border_box.x + translate.0 + shadow.offset_x - spread; let y = border_box.y + translate.1 + shadow.offset_y - spread; let w = border_box.width + spread * 2.0; let h = border_box.height + spread * 2.0; if w <= 0.0 || h <= 0.0 { return; }
let radii = [ (layout_box.border_radii[0] + spread).max(0.0), (layout_box.border_radii[1] + spread).max(0.0), (layout_box.border_radii[2] + spread).max(0.0), (layout_box.border_radii[3] + spread).max(0.0), ]; list.push(PaintCommand::BoxShadow { x, y, width: w, height: h, radii, blur_radius: blur, color: shadow.color, });}
fn paint_shadow_layer( layout_box: &LayoutBox, shadow: &BoxShadow, border_box: Rect, list: &mut DisplayList, translate: (f32, f32), expansion: f32, alpha_factor: f32,) { let outset = shadow.spread_radius + expansion; let x = border_box.x + translate.0 + shadow.offset_x - outset; let y = border_box.y + translate.1 + shadow.offset_y - outset; let w = border_box.width + outset * 2.0; let h = border_box.height + outset * 2.0; if w <= 0.0 || h <= 0.0 { return; }
let mut color = shadow.color; color.a = ((color.a as f32 * alpha_factor).round()).clamp(0.0, 255.0) as u8; if color.a == 0 { return; }
let radii = [ layout_box.border_radii[0] + outset, layout_box.border_radii[1] + outset, layout_box.border_radii[2] + outset, layout_box.border_radii[3] + outset, ]; fill_rounded_rect(list, x, y, w, h, radii, color);}
fn paint_background(layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: f32) { let bg = layout_box.background_color; // CSS default `background-clip: border-box`: the background fills the // border box. Borders paint on top, so for solid borders the result is // identical to padding-box clipping, but transparent/gapped border // styles (dotted, dashed) show the background behind them. let bb = border_box(layout_box); if bb.width <= 0.0 || bb.height <= 0.0 { return; } if has_border_radius(layout_box) { let radii = layout_box.border_radii; // Rounded background fills the border box; a uniform solid border is // then stroked on top as a rounded ring (`paint_borders` skips it). if bg.a > 0 { fill_rounded_rect(list, bb.x + tx, bb.y + ty, bb.width, bb.height, radii, bg); } if let Some((bw, bc)) = uniform_solid_border(layout_box) { stroke_rounded_rect( list, bb.x + tx, bb.y + ty, bb.width, bb.height, radii, bw, bc, ); } } else if bg.a > 0 { list.push(PaintCommand::FillRect { x: bb.x + tx, y: bb.y + ty, width: bb.width, height: bb.height, color: bg, }); } if let Some(gradient) = &layout_box.background_gradient { list.push(PaintCommand::LinearGradient { x: bb.x + tx, y: bb.y + ty, width: bb.width, height: bb.height, gradient: gradient.clone(), }); } for index in (0..layout_box.background_radial_gradients.len()).rev() { let gradient = &layout_box.background_radial_gradients[index]; list.push(PaintCommand::RadialGradient { x: bb.x + tx, y: bb.y + ty, width: bb.width, height: bb.height, position_x: background_layer_position( &layout_box.background_position_x_layers, index, layout_box.background_position_x, ), position_y: background_layer_position( &layout_box.background_position_y_layers, index, layout_box.background_position_y, ), repeat: layout_box.background_repeat, gradient: gradient.clone(), }); } if layout_box.background_image.is_some() { if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { list.push(PaintCommand::DrawBackgroundImage { x: bb.x + tx, y: bb.y + ty, width: bb.width, height: bb.height, node_id, size: layout_box.background_size, position_x: layout_box.background_position_x, position_y: layout_box.background_position_y, repeat: layout_box.background_repeat, }); } }}
fn background_layer_position( layers: &[LengthOrAuto], index: usize, fallback: LengthOrAuto,) -> LengthOrAuto { if layers.is_empty() { return fallback; } layers[index % layers.len()]}
fn paint_borders(layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: f32) { // A uniform solid border on a rounded box is painted as a ring by // `paint_background`, so skip the square per-side painter here. Non-uniform // rounded borders fall through to the square approximation below. if has_border_radius(layout_box) && uniform_solid_border(layout_box).is_some() { return; } let b = &layout_box.border; let r = &layout_box.rect; let styles = &layout_box.border_styles; let colors = &layout_box.border_colors;
// Border box starts at content origin minus padding and border. let bx = r.x - layout_box.padding.left - b.left + tx; let by = r.y - layout_box.padding.top - b.top + ty; let bw = b.left + layout_box.padding.left + r.width + layout_box.padding.right + b.right; let bh = b.top + layout_box.padding.top + r.height + layout_box.padding.bottom + b.bottom;
if paint_zero_sized_border_triangle(layout_box, list, bx, by, bw, bh) { return; }
// Top border if b.top > 0.0 { paint_border_side(list, bx, by, bw, b.top, true, styles[0], colors[0]); } // Right border if b.right > 0.0 { paint_border_side( list, bx + bw - b.right, by, b.right, bh, false, styles[1], colors[1], ); } // Bottom border if b.bottom > 0.0 { paint_border_side( list, bx, by + bh - b.bottom, bw, b.bottom, true, styles[2], colors[2], ); } // Left border if b.left > 0.0 { paint_border_side(list, bx, by, b.left, bh, false, styles[3], colors[3]); }}
fn paint_zero_sized_border_triangle( layout_box: &LayoutBox, list: &mut DisplayList, bx: f32, by: f32, bw: f32, bh: f32,) -> bool { if layout_box.rect.width > 0.0 || layout_box.rect.height > 0.0 || layout_box.padding.left > 0.0 || layout_box.padding.right > 0.0 || layout_box.padding.top > 0.0 || layout_box.padding.bottom > 0.0 { return false; }
let b = &layout_box.border; let styles = &layout_box.border_styles; let colors = &layout_box.border_colors; let sides = [ (b.top, styles[0], colors[0]), (b.right, styles[1], colors[1]), (b.bottom, styles[2], colors[2]), (b.left, styles[3], colors[3]), ]; let visible: Vec<usize> = sides .iter() .enumerate() .filter_map(|(idx, (width, style, color))| { (*width > 0.0 && border_style_paints_solid(*style) && color.a > 0).then_some(idx) }) .collect(); if visible.len() != 1 { return false; }
paint_border_triangle(list, bx, by, bw, bh, visible[0], sides[visible[0]].2); true}
fn border_style_paints_solid(style: BorderStyle) -> bool { matches!( style, BorderStyle::Solid | BorderStyle::Groove | BorderStyle::Ridge | BorderStyle::Inset | BorderStyle::Outset )}
fn paint_border_triangle( list: &mut DisplayList, x: f32, y: f32, width: f32, height: f32, side: usize, color: Color,) { if width <= 0.0 || height <= 0.0 { return; } let steps = match side { 0 | 2 => height.ceil().max(1.0) as u32, _ => width.ceil().max(1.0) as u32, }; let cx = x + width / 2.0; let cy = y + height / 2.0; for i in 0..steps { let start = i as f32 / steps as f32; let end = (i + 1) as f32 / steps as f32; let mid = (start + end) / 2.0; match side { // border-top: base at top, apex at bottom. 0 => { let w = width * (1.0 - mid); list.push(PaintCommand::FillRect { x: cx - w / 2.0, y: y + height * start, width: w, height: height * (end - start), color, }); } // border-right: base at right, apex at left. 1 => { let h = height * mid; list.push(PaintCommand::FillRect { x: x + width * start, y: cy - h / 2.0, width: width * (end - start), height: h, color, }); } // border-bottom: base at bottom, apex at top. 2 => { let w = width * mid; list.push(PaintCommand::FillRect { x: cx - w / 2.0, y: y + height * start, width: w, height: height * (end - start), color, }); } // border-left: base at left, apex at right. _ => { let h = height * (1.0 - mid); list.push(PaintCommand::FillRect { x: x + width * start, y: cy - h / 2.0, width: width * (end - start), height: h, color, }); } } }}
/// Paint one side of a border. `horizontal` selects whether the strip runs/// along the x axis (top/bottom) or the y axis (left/right). The strip/// extends across the corners; dashed/dotted patterns start at the strip's/// origin and any overlap with adjacent sides repaints the same color so/// corners join cleanly.#[allow(clippy::too_many_arguments)]fn paint_border_side( list: &mut DisplayList, x: f32, y: f32, width: f32, height: f32, horizontal: bool, style: BorderStyle, color: Color,) { if width <= 0.0 || height <= 0.0 { return; } match style { BorderStyle::None | BorderStyle::Hidden => {} BorderStyle::Dashed => { let thickness = if horizontal { height } else { width }; let length = if horizontal { width } else { height }; let dash = (thickness * 3.0).max(1.0); let gap = thickness.max(1.0); let period = dash + gap; let mut cur = 0.0f32; while cur < length { let len = dash.min(length - cur); if horizontal { list.push(PaintCommand::FillRect { x: x + cur, y, width: len, height, color, }); } else { list.push(PaintCommand::FillRect { x, y: y + cur, width, height: len, color, }); } cur += period; } } BorderStyle::Dotted => { let thickness = if horizontal { height } else { width }; let length = if horizontal { width } else { height }; let dot = thickness.max(1.0); let gap = thickness.max(1.0); let period = dot + gap; let mut cur = 0.0f32; while cur + dot <= length + 0.001 { if horizontal { list.push(PaintCommand::FillRect { x: x + cur, y, width: dot, height, color, }); } else { list.push(PaintCommand::FillRect { x, y: y + cur, width, height: dot, color, }); } cur += period; } } BorderStyle::Double => { let thickness = if horizontal { height } else { width }; if thickness < 3.0 { // Spec: when border-width is too small to split into three // visible bands, render as a single solid line. list.push(PaintCommand::FillRect { x, y, width, height, color, }); return; } let line = (thickness / 3.0).floor().max(1.0); if horizontal { list.push(PaintCommand::FillRect { x, y, width, height: line, color, }); list.push(PaintCommand::FillRect { x, y: y + height - line, width, height: line, color, }); } else { list.push(PaintCommand::FillRect { x, y, width: line, height, color, }); list.push(PaintCommand::FillRect { x: x + width - line, y, width: line, height, color, }); } } BorderStyle::Solid | BorderStyle::Groove | BorderStyle::Ridge | BorderStyle::Inset | BorderStyle::Outset => { list.push(PaintCommand::FillRect { x, y, width, height, color, }); } }}
fn paint_text(layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: f32) { for line in &layout_box.lines { let color = line.color; let font_size = line.font_size;
// Record index before pushing glyphs so we can insert the background // before the text in painter's order. let glyph_idx = list.len();
// Create a translated copy of the text line. let mut translated_line = line.clone(); translated_line.x += tx; translated_line.y += ty;
for shadow in &line.text_shadows { if shadow.color.a == 0 { continue; } let mut shadow_line = translated_line.clone(); shadow_line.x += shadow.offset_x; shadow_line.y += shadow.offset_y; shadow_line.color = shadow.color; list.push(PaintCommand::DrawGlyphs { line: shadow_line, font_size, color: shadow.color, }); }
list.push(PaintCommand::DrawGlyphs { line: translated_line, font_size, color, });
// Draw text decoration as a 1px line. Position depends on the variant: // Underline — just below the baseline. // Overline — at the top of the text line. // LineThrough — around the x-height (mid-line). if line.width > 0.0 { let baseline_y = line.y + ty + line.baseline_offset; let decoration_y = match line.text_decoration { TextDecoration::Underline => Some(baseline_y + 2.0), TextDecoration::Overline => Some(line.y + ty), TextDecoration::LineThrough => Some(baseline_y - font_size * 0.3), TextDecoration::None => None, }; if let Some(y) = decoration_y { list.push(PaintCommand::FillRect { x: line.x + tx, y, width: line.width, height: 1.0, color, }); } }
// Draw inline background if not transparent. if line.background_color.a > 0 && line.width > 0.0 { list.insert( glyph_idx, PaintCommand::FillRect { x: line.x + tx, y: line.y + ty, width: line.width, height: line.line_height, color: line.background_color, }, ); } }}
/// Paint the list-item marker (bullet) for a box with `display: list-item`.////// The marker is drawn as a "•" glyph positioned just inside the start of the/// content area, offset to its left. Per CSS lists, the marker sits in the/// padding-left area: when the UA stylesheet sets `padding-left: 40px` on/// `<ul>`/`<ol>`, the bullet lands roughly in the middle of that gutter.fn paint_list_marker(layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: f32) { if layout_box.display != Display::ListItem { return; } let font_size = layout_box.font_size; // Marker is centered ~1em to the left of the content edge so it sits in // the gutter created by the parent `<ul>`/`<ol>` padding-left. let marker_x = layout_box.rect.x - font_size * 1.0 + tx; let marker_y = layout_box.rect.y + ty; let marker_text = "\u{2022}".to_string(); // BULLET (•) let marker_width = font_size * 0.6; list.push(PaintCommand::DrawGlyphs { line: TextLine { text: marker_text, x: marker_x, y: marker_y, width: marker_width, font_size, baseline_offset: font_size, line_height: font_size * 1.2, font_weight: layout_box.font_weight, letter_spacing: 0.0, font_family: String::new(), color: layout_box.color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, background_color: Color::new(0, 0, 0, 0), bold: layout_box.bold, italic: false, }, font_size, color: layout_box.color, });}
/// Paint scroll bars for a scrollable box.fn paint_scrollbars( layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: f32, scroll_state: &ScrollState,) { let pad = padding_box(layout_box); let viewport_height = pad.height; let content_height = layout_box.content_height;
// Determine whether scroll bars should be shown. let show_vertical = match layout_box.overflow { Overflow::Scroll => true, Overflow::Auto => content_height > viewport_height, _ => false, };
if !show_vertical || viewport_height <= 0.0 { return; }
// Scroll bar track: right edge of the padding box. let track_x = pad.x + pad.width - SCROLLBAR_WIDTH + tx; let track_y = pad.y + ty; let track_height = viewport_height;
// Paint the track background. list.push(PaintCommand::FillRect { x: track_x, y: track_y, width: SCROLLBAR_WIDTH, height: track_height, color: SCROLLBAR_TRACK_COLOR, });
// Compute thumb size and position. let max_content = content_height.max(viewport_height); let thumb_ratio = viewport_height / max_content; let thumb_height = (thumb_ratio * track_height).max(20.0).min(track_height);
// Get current scroll offset. let scroll_y = node_id_from_box_type(&layout_box.box_type) .and_then(|id| scroll_state.get(&id)) .map(|&(_, sy)| sy) .unwrap_or(0.0);
let max_scroll = (content_height - viewport_height).max(0.0); let scroll_ratio = if max_scroll > 0.0 { scroll_y / max_scroll } else { 0.0 }; let thumb_y = track_y + scroll_ratio * (track_height - thumb_height);
// Paint the thumb. list.push(PaintCommand::FillRect { x: track_x, y: thumb_y, width: SCROLLBAR_WIDTH, height: thumb_height, color: SCROLLBAR_THUMB_COLOR, });}
// ---------------------------------------------------------------------------// Form control painting// ---------------------------------------------------------------------------
/// Colors for native-style form control rendering.const FC_BORDER_COLOR: Color = Color { r: 118, g: 118, b: 118, a: 255,};const FC_BG_COLOR: Color = Color { r: 255, g: 255, b: 255, a: 255,};const FC_BUTTON_BG: Color = Color { r: 239, g: 239, b: 239, a: 255,};const FC_BUTTON_BORDER_LIGHT: Color = Color { r: 200, g: 200, b: 200, a: 255,};const FC_BUTTON_BORDER_DARK: Color = Color { r: 133, g: 133, b: 133, a: 255,};const FC_CHECK_COLOR: Color = Color { r: 0, g: 0, b: 0, a: 255,};const FC_DISABLED_BG: Color = Color { r: 235, g: 235, b: 228, a: 255,};const FC_DISABLED_TEXT: Color = Color { r: 169, g: 169, b: 169, a: 255,};const FC_TEXT_COLOR: Color = Color { r: 0, g: 0, b: 0, a: 255,};const FC_PLACEHOLDER_TEXT: Color = Color { r: 117, g: 117, b: 117, a: 255,};/// Focus ring color (blue outline, matching typical browser defaults).const FC_FOCUS_RING_COLOR: Color = Color { r: 0, g: 95, b: 204, a: 255,};/// Text selection highlight color (standard macOS blue selection).const FC_SELECTION_COLOR: Color = Color { r: 179, g: 215, b: 254, a: 255,};/// Text cursor (caret) color.const FC_CURSOR_COLOR: Color = Color { r: 0, g: 0, b: 0, a: 255,};/// Dropdown menu background.const FC_DROPDOWN_BG: Color = Color { r: 255, g: 255, b: 255, a: 255,};/// Dropdown highlighted option background.const FC_DROPDOWN_HIGHLIGHT: Color = Color { r: 0, g: 95, b: 204, a: 255,};/// Dropdown highlighted option text color.const FC_DROPDOWN_HIGHLIGHT_TEXT: Color = Color { r: 255, g: 255, b: 255, a: 255,};/// Dropdown border color.const FC_DROPDOWN_BORDER: Color = Color { r: 180, g: 180, b: 180, a: 255,};/// Dropdown shadow color (semi-transparent).const FC_DROPDOWN_SHADOW: Color = Color { r: 0, g: 0, b: 0, a: 40,};/// Optgroup label text color.const FC_OPTGROUP_TEXT: Color = Color { r: 100, g: 100, b: 100, a: 255,};
/// Paint a form control with native-style appearance.fn paint_form_control( layout_box: &LayoutBox, fc: &FormControlInfo, list: &mut DisplayList, tx: f32, ty: f32,) { let bb = border_box(layout_box); if bb.width <= 0.0 || bb.height <= 0.0 { return; }
match fc.control_type { FormControlType::TextInput | FormControlType::Password => { paint_text_input(layout_box, fc, list, tx, ty); } FormControlType::Textarea => { paint_textarea(layout_box, fc, list, tx, ty); } FormControlType::Checkbox => { paint_checkbox(layout_box, fc, list, tx, ty); } FormControlType::Radio => { paint_radio(layout_box, fc, list, tx, ty); } FormControlType::Submit | FormControlType::Reset | FormControlType::Button => { paint_button(layout_box, fc, list, tx, ty); } FormControlType::Select => { paint_select(layout_box, fc, list, tx, ty); } }
// Paint focus ring around focused controls. if fc.focused { paint_focus_ring(layout_box, list, tx, ty); }}
/// Paint a focus ring (blue outline) around a focused form control.fn paint_focus_ring(layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: f32) { let bb = border_box(layout_box); // The focus ring sits 1px outside the border box. let offset = 1.0f32; let thickness = 2.0f32; let x = bb.x + tx - offset; let y = bb.y + ty - offset; let w = bb.width + offset * 2.0; let h = bb.height + offset * 2.0; let color = FC_FOCUS_RING_COLOR;
// A rounded control gets a rounded (concentric) focus ring; the outer // radius grows by the offset so the ring stays parallel to the border. if has_border_radius(layout_box) { let radii = [ layout_box.border_radii[0] + offset, layout_box.border_radii[1] + offset, layout_box.border_radii[2] + offset, layout_box.border_radii[3] + offset, ]; stroke_rounded_rect(list, x, y, w, h, radii, thickness, color); return; }
// Top list.push(PaintCommand::FillRect { x, y, width: w, height: thickness, color, }); // Bottom list.push(PaintCommand::FillRect { x, y: y + h - thickness, width: w, height: thickness, color, }); // Left list.push(PaintCommand::FillRect { x, y, width: thickness, height: h, color, }); // Right list.push(PaintCommand::FillRect { x: x + w - thickness, y, width: thickness, height: h, color, });}
/// Paint a text input or password field: inset border, white background, value text.fn paint_text_input( layout_box: &LayoutBox, fc: &FormControlInfo, list: &mut DisplayList, tx: f32, ty: f32,) { let bb = border_box(layout_box); let x = bb.x + tx; let y = bb.y + ty; let w = bb.width; let h = bb.height;
let bg = if fc.disabled { FC_DISABLED_BG } else { FC_BG_COLOR }; let is_placeholder = fc.value.is_empty() && !fc.placeholder.is_empty(); let text_color = if fc.disabled { FC_DISABLED_TEXT } else if is_placeholder { FC_PLACEHOLDER_TEXT } else { FC_TEXT_COLOR };
if layout_box.appearance == Appearance::None { paint_background(layout_box, list, tx, ty); paint_borders(layout_box, list, tx, ty); } else if has_border_radius(layout_box) { // CSS-styled input (e.g. the browser chrome address bar): honor the // author's background and rounded border instead of the default inset // look. A transparent CSS background falls back to the field default. let fill = if layout_box.background_color.a > 0 { layout_box.background_color } else { bg }; let radii = layout_box.border_radii; fill_rounded_rect(list, x, y, w, h, radii, fill); if let Some((bw, bc)) = uniform_solid_border(layout_box) { stroke_rounded_rect(list, x, y, w, h, radii, bw, bc); } } else { // Background list.push(PaintCommand::FillRect { x, y, width: w, height: h, color: bg, });
// Inset border: darker on top/left, lighter on bottom/right. let border = 1.0f32; // Top list.push(PaintCommand::FillRect { x, y, width: w, height: border, color: FC_BUTTON_BORDER_DARK, }); // Left list.push(PaintCommand::FillRect { x, y, width: border, height: h, color: FC_BUTTON_BORDER_DARK, }); // Bottom list.push(PaintCommand::FillRect { x, y: y + h - border, width: w, height: border, color: FC_BUTTON_BORDER_LIGHT, }); // Right list.push(PaintCommand::FillRect { x: x + w - border, y, width: border, height: h, color: FC_BUTTON_BORDER_LIGHT, }); }
// Content area for text. A single line of text is vertically centered in // the content box; `text_y` is the line-box top, and the caret/selection // span the line height (not the whole content box). let font_size = layout_box.font_size; let content_h = layout_box.rect.height; let line_h = font_size * 1.2; let v_offset = ((content_h - line_h) / 2.0).max(0.0); let text_x = layout_box.rect.x + tx; let text_y = layout_box.rect.y + ty + v_offset;
// For password fields, map byte offsets through the bullet-character mapping. let display_text = if is_placeholder { fc.placeholder.clone() } else if fc.control_type == FormControlType::Password { "\u{2022}".repeat(fc.value.chars().count()) } else { fc.value.clone() };
// Approximate character width (monospace fallback when glyph-accurate // metrics are unavailable, e.g. controls laid out without a font). let char_width = font_size * 0.6; let metrics = layout_box.input_text_metrics;
// Paint selection highlight (behind text) when focused and selection exists. if fc.focused && fc.cursor != fc.selection_anchor { let (sel_x, sel_w) = if let Some(m) = metrics { (text_x + m.sel_start_x, m.sel_end_x - m.sel_start_x) } else { let (sel_start, sel_end) = if fc.cursor <= fc.selection_anchor { (fc.cursor, fc.selection_anchor) } else { (fc.selection_anchor, fc.cursor) }; let start_chars = char_count_for_bytes(&fc.value, sel_start); let end_chars = char_count_for_bytes(&fc.value, sel_end); ( text_x + start_chars as f32 * char_width, (end_chars - start_chars) as f32 * char_width, ) }; list.push(PaintCommand::FillRect { x: sel_x, y: text_y, width: sel_w, height: line_h, color: FC_SELECTION_COLOR, }); }
// Value text (clipped to content area). if !display_text.is_empty() { list.push(PaintCommand::DrawGlyphs { line: TextLine { text: display_text, x: text_x, y: text_y, width: layout_box.rect.width, font_size, baseline_offset: font_size, line_height: font_size * 1.2, font_weight: layout_box.font_weight, letter_spacing: 0.0, font_family: String::new(), color: text_color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, background_color: Color::new(0, 0, 0, 0), bold: layout_box.bold, italic: layout_box.italic, }, font_size, color: text_color, }); }
// Paint cursor (caret) when focused and no selection. if fc.focused && fc.cursor == fc.selection_anchor { let cursor_x = if let Some(m) = metrics { text_x + m.caret_x } else { let cursor_chars = char_count_for_bytes(&fc.value, fc.cursor); text_x + cursor_chars as f32 * char_width }; let cursor_w = 1.0f32; list.push(PaintCommand::FillRect { x: cursor_x, y: text_y, width: cursor_w, height: line_h, color: FC_CURSOR_COLOR, }); }}
/// Count the number of Unicode characters in `s[..byte_offset]`.fn char_count_for_bytes(s: &str, byte_offset: usize) -> usize { let clamped = byte_offset.min(s.len()); s[..clamped].chars().count()}
/// Paint a textarea: same style as text input but taller.fn paint_textarea( layout_box: &LayoutBox, fc: &FormControlInfo, list: &mut DisplayList, tx: f32, ty: f32,) { // Reuse text input painting — same visual style. paint_text_input(layout_box, fc, list, tx, ty);}
/// Paint a checkbox: small bordered square with optional checkmark.fn paint_checkbox( layout_box: &LayoutBox, fc: &FormControlInfo, list: &mut DisplayList, tx: f32, ty: f32,) { let bb = border_box(layout_box); let x = bb.x + tx; let y = bb.y + ty; let size = bb.width.min(bb.height);
let bg = if fc.disabled { FC_DISABLED_BG } else { FC_BG_COLOR };
// Background list.push(PaintCommand::FillRect { x, y, width: size, height: size, color: bg, });
// Border let border = 1.0f32; let bc = FC_BORDER_COLOR; // Top list.push(PaintCommand::FillRect { x, y, width: size, height: border, color: bc, }); // Bottom list.push(PaintCommand::FillRect { x, y: y + size - border, width: size, height: border, color: bc, }); // Left list.push(PaintCommand::FillRect { x, y, width: border, height: size, color: bc, }); // Right list.push(PaintCommand::FillRect { x: x + size - border, y, width: border, height: size, color: bc, });
// Indeterminate indicator: horizontal dash in the center. if fc.indeterminate { let ind_color = if fc.disabled { FC_DISABLED_TEXT } else { FC_CHECK_COLOR }; let inset = (size * 0.25).max(2.0); let thick = (size * 0.15).max(1.5); let dash_y = y + (size - thick) / 2.0; list.push(PaintCommand::FillRect { x: x + inset, y: dash_y, width: size - inset * 2.0, height: thick, color: ind_color, }); } // Checkmark when checked: draw a simple check using two rectangles // forming an "L" rotated, approximated as two thick lines. else if fc.checked { let check_color = if fc.disabled { FC_DISABLED_TEXT } else { FC_CHECK_COLOR }; let inset = (size * 0.2).max(2.0); let thick = (size * 0.15).max(1.5);
// Short leg of check (going down-right from left) let leg_x = x + inset; let leg_y = y + size * 0.45; let leg_w = size * 0.25; let leg_h = size * 0.35; list.push(PaintCommand::FillRect { x: leg_x, y: leg_y, width: thick, height: leg_h, color: check_color, });
// Long leg of check (going up-right) let arm_x = leg_x + thick; let arm_y = y + inset; list.push(PaintCommand::FillRect { x: arm_x, y: arm_y, width: leg_w, height: thick, color: check_color, });
// Diagonal approximation: draw from bottom-left to top-right as // a series of small filled rects. // Short descending leg: from (inset, center) to (center, bottom-inset) let cx = x + size * 0.38; let cy = y + size - inset; let steps = ((size * 0.3) as i32).max(2); for i in 0..steps { let t = i as f32 / steps as f32; let rx = leg_x + t * (cx - leg_x); let ry = leg_y + t * (cy - leg_y); list.push(PaintCommand::FillRect { x: rx, y: ry, width: thick, height: thick, color: check_color, }); } // Long ascending leg: from (center, bottom-inset) to (right-inset, top+inset) let ex = x + size - inset; let ey = y + inset; let steps2 = ((size * 0.6) as i32).max(3); for i in 0..=steps2 { let t = i as f32 / steps2 as f32; let rx = cx + t * (ex - cx); let ry = cy + t * (ey - cy); list.push(PaintCommand::FillRect { x: rx, y: ry, width: thick, height: thick, color: check_color, }); } }}
/// Paint a radio button: bordered circle approximation with optional filled dot.fn paint_radio( layout_box: &LayoutBox, fc: &FormControlInfo, list: &mut DisplayList, tx: f32, ty: f32,) { let bb = border_box(layout_box); let x = bb.x + tx; let y = bb.y + ty; let size = bb.width.min(bb.height); let cx = x + size / 2.0; let cy = y + size / 2.0; let radius = size / 2.0;
let bg = if fc.disabled { FC_DISABLED_BG } else { FC_BG_COLOR };
// Draw circle approximation using filled rects in a circular pattern. // Outer circle (border). paint_circle_filled(list, cx, cy, radius, FC_BORDER_COLOR); // Inner circle (background). paint_circle_filled(list, cx, cy, radius - 1.0, bg);
// Selected dot. if fc.checked { let dot_color = if fc.disabled { FC_DISABLED_TEXT } else { FC_CHECK_COLOR }; paint_circle_filled(list, cx, cy, radius * 0.4, dot_color); }}
/// Approximate a filled circle using horizontal FillRect scanlines.fn paint_circle_filled(list: &mut DisplayList, cx: f32, cy: f32, radius: f32, color: Color) { if radius <= 0.0 { return; } let r = radius as i32; for dy in -r..=r { let dx = (radius * radius - (dy as f32 * dy as f32)).sqrt(); if dx > 0.0 { list.push(PaintCommand::FillRect { x: cx - dx, y: cy + dy as f32, width: dx * 2.0, height: 1.0, color, }); } }}
/// Paint a button (submit, reset, or generic): raised border with centered label.fn paint_button( layout_box: &LayoutBox, fc: &FormControlInfo, list: &mut DisplayList, tx: f32, ty: f32,) { let bb = border_box(layout_box); let x = bb.x + tx; let y = bb.y + ty; let w = bb.width; let h = bb.height;
let bg = if fc.disabled { FC_DISABLED_BG } else { FC_BUTTON_BG }; let text_color = if fc.disabled { FC_DISABLED_TEXT } else { FC_TEXT_COLOR };
// Background list.push(PaintCommand::FillRect { x, y, width: w, height: h, color: bg, });
// Raised border: lighter on top/left, darker on bottom/right. let border = 1.0f32; // Top (light) list.push(PaintCommand::FillRect { x, y, width: w, height: border, color: FC_BUTTON_BORDER_LIGHT, }); // Left (light) list.push(PaintCommand::FillRect { x, y, width: border, height: h, color: FC_BUTTON_BORDER_LIGHT, }); // Bottom (dark) list.push(PaintCommand::FillRect { x, y: y + h - border, width: w, height: border, color: FC_BUTTON_BORDER_DARK, }); // Right (dark) list.push(PaintCommand::FillRect { x: x + w - border, y, width: border, height: h, color: FC_BUTTON_BORDER_DARK, });
// Centered label text if !fc.value.is_empty() { let font_size = layout_box.font_size; let char_width = font_size * 0.6; let text_width = fc.value.len() as f32 * char_width; let text_x = x + (w - text_width) / 2.0; let text_y = y + (h - font_size) / 2.0; list.push(PaintCommand::DrawGlyphs { line: TextLine { text: fc.value.clone(), x: text_x, y: text_y, width: text_width, font_size, baseline_offset: font_size, line_height: font_size * 1.2, font_weight: layout_box.font_weight, letter_spacing: 0.0, font_family: String::new(), color: text_color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, background_color: Color::new(0, 0, 0, 0), bold: layout_box.bold, italic: layout_box.italic, }, font_size, color: text_color, }); }}
/// Paint a select control: either a dropdown button (single) or a listbox (multiple).fn paint_select( layout_box: &LayoutBox, fc: &FormControlInfo, list: &mut DisplayList, tx: f32, ty: f32,) { if fc.multiple { paint_select_listbox(layout_box, fc, list, tx, ty); } else { paint_select_button(layout_box, fc, list, tx, ty); }}
/// Paint a single-select dropdown button: bordered rectangle with selected text and arrow.fn paint_select_button( layout_box: &LayoutBox, fc: &FormControlInfo, list: &mut DisplayList, tx: f32, ty: f32,) { let bb = border_box(layout_box); let x = bb.x + tx; let y = bb.y + ty; let w = bb.width; let h = bb.height;
let bg = if fc.disabled { FC_DISABLED_BG } else { FC_BUTTON_BG }; let text_color = if fc.disabled { FC_DISABLED_TEXT } else { FC_TEXT_COLOR };
// Background list.push(PaintCommand::FillRect { x, y, width: w, height: h, color: bg, });
// Border paint_rect_border(list, x, y, w, h, 1.0, FC_BORDER_COLOR);
// Selected option text let font_size = layout_box.font_size; let text_x = layout_box.rect.x + tx; let text_y = layout_box.rect.y + ty; if !fc.value.is_empty() { list.push(PaintCommand::DrawGlyphs { line: TextLine { text: fc.value.clone(), x: text_x, y: text_y, width: layout_box.rect.width - 20.0, // leave room for arrow font_size, baseline_offset: font_size, line_height: font_size * 1.2, font_weight: layout_box.font_weight, letter_spacing: 0.0, font_family: String::new(), color: text_color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, background_color: Color::new(0, 0, 0, 0), bold: layout_box.bold, italic: layout_box.italic, }, font_size, color: text_color, }); }
// Dropdown arrow (▼) as a small triangle approximated with rects. let arrow_size = font_size * 0.4; let arrow_x = x + w - 14.0; let arrow_y = y + (h - arrow_size) / 2.0; let arrow_color = if fc.disabled { FC_DISABLED_TEXT } else { FC_TEXT_COLOR }; let rows = (arrow_size as i32).max(3); for i in 0..rows { let t = i as f32 / rows as f32; let row_width = arrow_size * (1.0 - t); let row_x = arrow_x + (arrow_size - row_width) / 2.0; list.push(PaintCommand::FillRect { x: row_x, y: arrow_y + i as f32, width: row_width, height: 1.0, color: arrow_color, }); }}
/// Paint a multiple-select listbox: bordered white box showing visible options.fn paint_select_listbox( layout_box: &LayoutBox, fc: &FormControlInfo, list: &mut DisplayList, tx: f32, ty: f32,) { let bb = border_box(layout_box); let x = bb.x + tx; let y = bb.y + ty; let w = bb.width; let h = bb.height;
let bg = if fc.disabled { FC_DISABLED_BG } else { FC_BG_COLOR }; let text_color = if fc.disabled { FC_DISABLED_TEXT } else { FC_TEXT_COLOR };
// Background list.push(PaintCommand::FillRect { x, y, width: w, height: h, color: bg, });
// Border paint_rect_border(list, x, y, w, h, 1.0, FC_BORDER_COLOR);
// Paint visible options let font_size = layout_box.font_size; let line_height = font_size * 1.2; let padding = 2.0f32; let mut oy = y + 1.0 + padding;
for (i, opt) in fc.options.iter().enumerate() { if oy + line_height > y + h - 1.0 { break; // Clipped }
let opt_text_color = if opt.disabled { FC_DISABLED_TEXT } else if opt.selected { FC_DROPDOWN_HIGHLIGHT_TEXT } else { text_color };
// Highlight selected options if opt.selected && !opt.is_group_label { list.push(PaintCommand::FillRect { x: x + 1.0, y: oy, width: w - 2.0, height: line_height, color: FC_DROPDOWN_HIGHLIGHT, }); }
// Highlighted (keyboard-navigated) option if fc.highlighted_index == Some(i) && !opt.is_group_label { paint_rect_border( list, x + 1.0, oy, w - 2.0, line_height, 1.0, FC_FOCUS_RING_COLOR, ); }
let indent = if opt.is_group_label { 0.0 } else { 4.0 }; let opt_color = if opt.is_group_label { FC_OPTGROUP_TEXT } else { opt_text_color };
list.push(PaintCommand::DrawGlyphs { line: TextLine { text: opt.text.clone(), x: x + padding + indent + 1.0, y: oy, width: w - padding * 2.0 - indent - 2.0, font_size: if opt.is_group_label { font_size * 0.9 } else { font_size }, baseline_offset: if opt.is_group_label { font_size * 0.9 } else { font_size }, line_height: if opt.is_group_label { font_size * 0.9 * 1.2 } else { font_size * 1.2 }, font_weight: if opt.is_group_label { 700.0 } else { 400.0 }, letter_spacing: 0.0, font_family: String::new(), color: opt_color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, background_color: Color::new(0, 0, 0, 0), bold: opt.is_group_label, italic: false, }, font_size: if opt.is_group_label { font_size * 0.9 } else { font_size }, color: opt_color, });
oy += line_height; }}
/// Paint a simple rectangular border (4 sides).fn paint_rect_border( list: &mut DisplayList, x: f32, y: f32, w: f32, h: f32, border: f32, color: Color,) { // Top list.push(PaintCommand::FillRect { x, y, width: w, height: border, color, }); // Bottom list.push(PaintCommand::FillRect { x, y: y + h - border, width: w, height: border, color, }); // Left list.push(PaintCommand::FillRect { x, y, width: border, height: h, color, }); // Right list.push(PaintCommand::FillRect { x: x + w - border, y, width: border, height: h, color, });}
/// Paint a dropdown overlay popup for an open single-select./// Called after the main paint pass so it renders on top of all content.fn paint_dropdown_overlay(dd: &PendingDropdown, list: &mut DisplayList) { let font_size = dd.font_size; let line_height = font_size * 1.2; let padding = 4.0f32; let border = 1.0f32;
let option_count = dd.options.len(); let menu_height = option_count as f32 * line_height + padding * 2.0 + border * 2.0; let menu_width = dd.width.max(100.0);
// Position below the select control. let menu_x = dd.x; let menu_y = dd.y + dd.height;
// Shadow (offset 2px down, slightly larger) list.push(PaintCommand::FillRect { x: menu_x + 2.0, y: menu_y + 2.0, width: menu_width, height: menu_height, color: FC_DROPDOWN_SHADOW, });
// Background list.push(PaintCommand::FillRect { x: menu_x, y: menu_y, width: menu_width, height: menu_height, color: FC_DROPDOWN_BG, });
// Border paint_rect_border( list, menu_x, menu_y, menu_width, menu_height, border, FC_DROPDOWN_BORDER, );
// Paint options let mut oy = menu_y + border + padding; for (i, opt) in dd.options.iter().enumerate() { let is_highlighted = dd.highlighted_index == Some(i);
// Highlight background if is_highlighted && !opt.is_group_label && !opt.disabled { list.push(PaintCommand::FillRect { x: menu_x + border, y: oy, width: menu_width - border * 2.0, height: line_height, color: FC_DROPDOWN_HIGHLIGHT, }); }
let text_color = if opt.is_group_label { FC_OPTGROUP_TEXT } else if opt.disabled { FC_DISABLED_TEXT } else if is_highlighted { FC_DROPDOWN_HIGHLIGHT_TEXT } else { FC_TEXT_COLOR };
let indent = if opt.is_group_label { padding } else { // Indent options within optgroups a bit more. padding + 8.0 };
let opt_font_size = if opt.is_group_label { font_size * 0.85 } else { font_size };
list.push(PaintCommand::DrawGlyphs { line: TextLine { text: opt.text.clone(), x: menu_x + indent, y: oy, width: menu_width - indent - padding, font_size: opt_font_size, baseline_offset: opt_font_size, line_height: opt_font_size * 1.2, font_weight: if opt.is_group_label { 700.0 } else { 400.0 }, letter_spacing: 0.0, font_family: String::new(), color: text_color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, background_color: Color::new(0, 0, 0, 0), bold: opt.is_group_label, italic: false, }, font_size: opt_font_size, color: text_color, });
oy += line_height; }}
pub(crate) fn resolve_background_image_size( size: BackgroundSize, box_width: f32, box_height: f32, intrinsic_width: f32, intrinsic_height: f32,) -> (f32, f32) { match size { BackgroundSize::Cover => { let scale = (box_width / intrinsic_width).max(box_height / intrinsic_height); (intrinsic_width * scale, intrinsic_height * scale) } BackgroundSize::Contain => { let scale = (box_width / intrinsic_width).min(box_height / intrinsic_height); (intrinsic_width * scale, intrinsic_height * scale) } BackgroundSize::Explicit(w, h) => { let resolved_w = resolve_background_length(w, box_width); let resolved_h = resolve_background_length(h, box_height); match (resolved_w, resolved_h) { (Some(w), Some(h)) => (w, h), (Some(w), None) => (w, w * intrinsic_height / intrinsic_width), (None, Some(h)) => (h * intrinsic_width / intrinsic_height, h), (None, None) => (intrinsic_width, intrinsic_height), } } BackgroundSize::Auto => (intrinsic_width, intrinsic_height), }}
fn resolve_background_length(value: LengthOrAuto, reference: f32) -> Option<f32> { match value { LengthOrAuto::Length(px) => Some(px), LengthOrAuto::Percentage(p) => Some(p / 100.0 * reference), LengthOrAuto::Calc(px, pct) => Some(px + pct / 100.0 * reference), LengthOrAuto::ClampCalc { min_px, px_offset, pct, max_px, } => Some( (px_offset + pct / 100.0 * reference) .max(min_px) .min(max_px), ), LengthOrAuto::FitContent | LengthOrAuto::Auto => None, }}
pub(crate) fn background_position_offset( value: LengthOrAuto, free_space: f32, reference: f32,) -> f32 { match value { LengthOrAuto::Percentage(p) => p / 100.0 * free_space, LengthOrAuto::Length(px) => px, LengthOrAuto::Calc(px, pct) => px + pct / 100.0 * reference, LengthOrAuto::ClampCalc { min_px, px_offset, pct, max_px, } => (px_offset + pct / 100.0 * reference) .max(min_px) .min(max_px), LengthOrAuto::FitContent | LengthOrAuto::Auto => 0.0, }}
#[cfg(test)]mod tests { use super::*; use we_dom::Document; use we_style::computed::{extract_stylesheets, resolve_styles}; use we_text::font::Font;
fn test_font() -> Font { let paths = [ "/System/Library/Fonts/Geneva.ttf", "/System/Library/Fonts/Monaco.ttf", ]; for path in &paths { let p = std::path::Path::new(path); if p.exists() { return Font::from_file(p).expect("failed to parse font"); } } panic!("no test font found"); }
fn layout_doc(doc: &Document) -> we_layout::LayoutTree { let font = test_font(); let sheets = extract_stylesheets(doc); let styled = resolve_styles(doc, &sheets, (800.0, 600.0)).unwrap(); we_layout::layout( &styled, doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), ) } #[test] fn display_list_from_empty_layout() { let doc = Document::new(); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)); if let Some(styled) = styled { let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), ); let list = build_display_list(&tree); assert!(list.len() <= 1); } }
#[test] fn transform_scale_zero_suppresses_subtree_paint() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.collapsed { transform: scale(0); width: 100px; height: 50px; background-color: red; }.collapsed .child { width: 100px; height: 50px; background-color: blue; }.visible { width: 100px; height: 50px; background-color: green; }</style></head><body><div class="collapsed"><div class="child"></div></div><div class="visible"></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_fill = |color| { list.iter().any(|cmd| { matches!( cmd, PaintCommand::FillRect { color: fill, .. } if *fill == color ) }) };
assert!(!has_fill(Color::rgb(255, 0, 0))); assert!(!has_fill(Color::rgb(0, 0, 255))); assert!(has_fill(Color::rgb(0, 128, 0))); }
#[test] fn transform_translate_offsets_subtree_paint() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.translated { transform: translate(25px, 10px); width: 100px; height: 50px; }.translated .child { width: 30px; height: 20px; background-color: blue; }</style></head><body><div class="translated"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let child_rect = list .iter() .find_map(|cmd| match cmd { PaintCommand::FillRect { x, y, width, height, color, } if *color == Color::rgb(0, 0, 255) => Some((*x, *y, *width, *height)), _ => None, }) .expect("child fill rect");
assert_eq!(child_rect, (25.0, 10.0, 30.0, 20.0)); }
#[test] fn canvas_background_propagates_from_body() { // example.com pattern: `body { background:#eee }`, html transparent. // The body background must propagate to the whole viewport canvas. let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); doc.set_attribute(body, "style", "background:#eee"); doc.append_child(root, html); doc.append_child(html, body);
let tree = layout_doc(&doc); assert_eq!( canvas_background_color(&tree), Some(Color::rgb(0xee, 0xee, 0xee)) ); }
#[test] fn canvas_background_prefers_root_over_body() { // When the root (html) has its own background, it wins over the body's. let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); doc.set_attribute(html, "style", "background:#123456"); doc.set_attribute(body, "style", "background:#abcdef"); doc.append_child(root, html); doc.append_child(html, body);
let tree = layout_doc(&doc); assert_eq!( canvas_background_color(&tree), Some(Color::rgb(0x12, 0x34, 0x56)) ); }
#[test] fn canvas_background_none_when_transparent() { // No author background anywhere: callers keep their default canvas. let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); doc.append_child(root, html); doc.append_child(html, body);
let tree = layout_doc(&doc); assert_eq!(canvas_background_color(&tree), None); }
#[test] fn display_list_has_background_and_text() { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let p = doc.create_element("p"); let text = doc.create_text("Hello world"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, p); doc.append_child(p, text);
let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_text = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { .. }));
assert!(has_text, "should have at least one DrawGlyphs"); }
#[test] fn inline_flex_svg_icon_emits_draw_image() { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let icon = doc.create_element("i"); doc.set_attribute(icon, "class", "icon-component"); let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); doc.set_attribute(svg, "width", "24"); doc.set_attribute(svg, "height", "24"); doc.set_attribute(svg, "viewBox", "0 0 24 24"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, icon); doc.append_child(icon, svg);
let font = test_font(); let ss = we_css::parser::Parser::parse( "body{margin:0}.icon-component{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px}.icon-component svg{width:100%;height:auto}", ); let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let mut sizes = std::collections::HashMap::new(); sizes.insert(svg, we_layout::ReplacedContent::Size(24.0, 24.0)); let tree = we_layout::layout(&styled, &doc, 800.0, 600.0, &font, &sizes); let list = build_display_list(&tree);
let draw = list .iter() .find_map(|cmd| match cmd { PaintCommand::DrawImage { node_id, x, y, width, height, .. } if *node_id == svg => Some((*x, *y, *width, *height)), _ => None, }) .expect("svg draw image command"); assert_eq!(draw, (0.0, 0.0, 24.0, 24.0)); }
#[test] fn paint_heading_produces_larger_glyphs() { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let h1 = doc.create_element("h1"); let h1_text = doc.create_text("Big"); let p = doc.create_element("p"); let p_text = doc.create_text("Small"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, h1); doc.append_child(h1, h1_text); doc.append_child(body, p); doc.append_child(p, p_text);
let tree = layout_doc(&doc); let list = build_display_list(&tree);
// There should be DrawGlyphs commands with different font sizes. let font_sizes: Vec<f32> = list .iter() .filter_map(|c| match c { PaintCommand::DrawGlyphs { font_size, .. } => Some(*font_size), _ => None, }) .collect();
assert!(font_sizes.len() >= 2, "should have at least 2 text lines"); // h1 has font_size 32, p has font_size 16. assert!( font_sizes.iter().any(|&s| s > 20.0), "should have a heading with large font size" ); assert!( font_sizes.iter().any(|&s| s < 20.0), "should have a paragraph with normal font size" ); } #[test] fn css_color_renders_correctly() { let html_str = r#"<!DOCTYPE html><html><head><style>p { color: red; }</style></head><body><p>Red text</p></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), );
let list = build_display_list(&tree); let text_colors: Vec<&Color> = list .iter() .filter_map(|c| match c { PaintCommand::DrawGlyphs { color, .. } => Some(color), _ => None, }) .collect();
assert!(!text_colors.is_empty()); // Text should be red. assert_eq!(*text_colors[0], Color::rgb(255, 0, 0)); }
#[test] fn css_text_shadow_paints_before_foreground_text() { let html_str = r#"<!DOCTYPE html><html><head><style>p { color: white; text-shadow: 2px 3px 4px rgba(0,0,0,.5); }</style></head><body><p>Shadowed</p></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let glyphs: Vec<(usize, Color, f32, f32)> = list .iter() .enumerate() .filter_map(|(idx, cmd)| match cmd { PaintCommand::DrawGlyphs { line, color, .. } if line.text == "Shadowed" => { Some((idx, *color, line.x, line.y)) } _ => None, }) .collect();
assert_eq!(glyphs.len(), 2, "shadow and foreground glyphs should paint"); assert_eq!(glyphs[0].1, Color::new(0, 0, 0, 128)); assert_eq!(glyphs[1].1, Color::rgb(255, 255, 255)); assert!(glyphs[0].0 < glyphs[1].0); assert!((glyphs[0].2 - glyphs[1].2 - 2.0).abs() < 0.01); assert!((glyphs[0].3 - glyphs[1].3 - 3.0).abs() < 0.01); }
#[test] fn css_background_color_renders() { let html_str = r#"<!DOCTYPE html><html><head><style>div { background-color: yellow; }</style></head><body><div>Content</div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), );
let list = build_display_list(&tree); let fill_colors: Vec<&Color> = list .iter() .filter_map(|c| match c { PaintCommand::FillRect { color, .. } => Some(color), _ => None, }) .collect();
// Should have a yellow fill rect for the div background. assert!(fill_colors.iter().any(|c| **c == Color::rgb(255, 255, 0))); }
#[test] fn css_background_image_emits_background_paint_command() { let html_str = r#"<!DOCTYPE html><html><head><style>div { width: 100px; height: 50px; background-image: url(hero.jpg); background-size: cover; background-position: center; }</style></head><body><div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), );
let list = build_display_list(&tree); assert!( list.iter() .any(|c| matches!(c, PaintCommand::DrawBackgroundImage { .. })), "background-image should produce a background image paint command" ); }
#[test] fn zero_sized_border_triangle_paints_tapered_strips() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.triangle { width: 0; height: 0; border-left: 3px solid transparent; border-right: 3px solid transparent; border-top: 4px solid black;}</style></head><body><div class="triangle"></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let black_rects: Vec<(f32, f32)> = list .iter() .filter_map(|cmd| match cmd { PaintCommand::FillRect { width, height, color, .. } if *color == Color::rgb(0, 0, 0) => Some((*width, *height)), _ => None, }) .collect();
assert!( black_rects.len() > 1, "triangle should be approximated by multiple tapered strips, got {black_rects:?}" ); assert!( !black_rects .iter() .any(|(w, h)| (*w - 6.0).abs() < 0.1 && (*h - 4.0).abs() < 0.1), "triangle must not paint a full 6x4 square: {black_rects:?}" ); let min_width = black_rects .iter() .map(|(w, _)| *w) .fold(f32::INFINITY, f32::min); let max_width = black_rects.iter().map(|(w, _)| *w).fold(0.0, f32::max); assert!(max_width > min_width, "strip widths should taper"); }
#[test] fn css_linear_gradient_emits_gradient_paint_command() { let html_str = r#"<!DOCTYPE html><html><head><style>div { width: 100px; height: 50px; background: linear-gradient(to bottom, rgba(0,0,0,.6), transparent); }</style></head><body><div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), );
let list = build_display_list(&tree); let gradient = list .iter() .find_map(|c| match c { PaintCommand::LinearGradient { width, height, gradient, .. } => Some((*width, *height, gradient)), _ => None, }) .expect("linear-gradient should produce a gradient paint command"); assert!((gradient.0 - 100.0).abs() < 0.5); assert!((gradient.1 - 50.0).abs() < 0.5); assert_eq!(gradient.2.stops[0].color, Color::new(0, 0, 0, 153)); assert_eq!(gradient.2.stops[1].color, Color::new(0, 0, 0, 0)); }
#[test] fn css_radial_gradient_emits_gradient_paint_command() { let html_str = r#"<!DOCTYPE html><html><head><style>div { width: 100px; height: 50px; background: radial-gradient(circle, rgba(87,41,255,.23) 0, transparent 49%); background-position-x: 60vw; background-position-y: -6vh;}</style></head><body><div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), );
let list = build_display_list(&tree); let gradient = list .iter() .find_map(|c| match c { PaintCommand::RadialGradient { width, height, position_x, position_y, repeat, gradient, .. } => Some((*width, *height, *position_x, *position_y, *repeat, gradient)), _ => None, }) .expect("radial-gradient should produce a gradient paint command"); assert!((gradient.0 - 100.0).abs() < 0.5); assert!((gradient.1 - 50.0).abs() < 0.5); assert_eq!(gradient.2, LengthOrAuto::Length(480.0)); assert_eq!(gradient.3, LengthOrAuto::Length(-36.0)); assert_eq!(gradient.4, BackgroundRepeat::Repeat); assert_eq!(gradient.5.stops[0].color, Color::new(87, 41, 255, 59)); assert_eq!(gradient.5.stops[1].color, Color::new(0, 0, 0, 0)); }
#[test] fn empty_before_pseudo_element_paints_gradient_box() { let html_str = r#"<!DOCTYPE html><html><head><style>html, body { margin: 0; }.hero { position: relative; width: 200px; height: 100px; background: white; }.hero::before { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 60px; background: linear-gradient(rgba(0,0,0,.6), transparent);}</style></head><body><div class="hero"></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let gradient = list .iter() .find_map(|c| match c { PaintCommand::LinearGradient { x, y, width, height, gradient, } => Some((*x, *y, *width, *height, gradient)), _ => None, }) .expect("empty ::before should paint its gradient background");
assert!((gradient.0 - 0.0).abs() < 0.5); assert!((gradient.1 - 0.0).abs() < 0.5); assert!((gradient.2 - 200.0).abs() < 0.5); assert!((gradient.3 - 60.0).abs() < 0.5); assert_eq!(gradient.4.stops[0].color, Color::new(0, 0, 0, 153)); assert_eq!(gradient.4.stops[1].color, Color::new(0, 0, 0, 0)); }
#[test] fn css_box_shadow_paints_before_rounded_background() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }div { width: 100px; height: 50px; background: white; border-radius: 10px; box-shadow: 0 2px 4px 1px rgba(0,0,0,.4);}</style></head><body><div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), );
let list = build_display_list(&tree); let shadow_index = list .iter() .position(|cmd| { matches!( cmd, PaintCommand::BoxShadow { color, .. } if color.r == 0 && color.g == 0 && color.b == 0 && color.a > 0 ) }) .expect("box-shadow should emit blurred shadow paint"); let background_index = list .iter() .position(|cmd| { matches!( cmd, PaintCommand::RoundedRect { color, .. } if *color == Color::rgb(255, 255, 255) ) }) .expect("rounded background should paint");
assert!( shadow_index < background_index, "outer shadow must paint behind the element background" ); }
#[test] fn hsl_flex_descendant_backgrounds_render_at_flex_positions() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { display: flex; width: 400px; }.item { width: 100px; height: 50px; }.inner { display: block; width: 20px; height: 20px; background-color: hsl(0, 0%, 89%); }</style></head><body><div class="container"> <div class="item"><section class="inner"></section></div> <div class="item"><section class="inner"></section></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let fills: Vec<(f32, f32, f32, f32)> = list .iter() .filter_map(|cmd| match cmd { PaintCommand::FillRect { x, y, width, height, color, } if *color == Color::rgb(227, 227, 227) => Some((*x, *y, *width, *height)), _ => None, }) .collect();
assert!( fills.iter().any(|(x, y, width, height)| { (*x - 0.0).abs() < 1.0 && (*y - 0.0).abs() < 1.0 && (*width - 20.0).abs() < 1.0 && (*height - 20.0).abs() < 1.0 }), "first hsl fill should paint at the first flex item origin: {fills:?}" ); assert!( fills.iter().any(|(x, y, width, height)| { (*x - 100.0).abs() < 1.0 && (*y - 0.0).abs() < 1.0 && (*width - 20.0).abs() < 1.0 && (*height - 20.0).abs() < 1.0 }), "second hsl fill should move with the second flex item: {fills:?}" ); }
#[test] fn background_extends_to_border_box() { // CSS default is `background-clip: border-box`: an element's // background-color must fill the entire border box (content + // padding + border area), not just the content rect. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.box { width: 100px; height: 50px; padding: 16px; border: 4px solid green; background-color: red; }</style></head><body><div class="box"></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let red_fill = list .iter() .find_map(|c| match c { PaintCommand::FillRect { x, y, width, height, color, } if *color == Color::rgb(255, 0, 0) => Some((*x, *y, *width, *height)), _ => None, }) .expect("background fill should be in display list");
// Border box is content (100x50) + padding (16+16) + border (4+4) // = 140 wide x 90 tall, starting at (0,0). assert_eq!(red_fill.0, 0.0, "background x should be at border box left"); assert_eq!(red_fill.1, 0.0, "background y should be at border box top"); assert_eq!( red_fill.2, 140.0, "background width should equal border-box width" ); assert_eq!( red_fill.3, 90.0, "background height should equal border-box height" ); }
#[test] fn border_rendering() { let html_str = r#"<!DOCTYPE html><html><head><style>div { border: 2px solid red; }</style></head><body><div>Bordered</div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), );
let list = build_display_list(&tree); let red_fills: Vec<_> = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(255, 0, 0))) .collect();
// Should have 4 border fills (top, right, bottom, left). assert_eq!(red_fills.len(), 4, "should have 4 border edges"); }
#[test] fn dashed_border_produces_multiple_segments_per_side() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0 } div { width: 100px; height: 60px; border: 2px dashed red; }</style></head><body><div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), ); let list = build_display_list(&tree); let red_fills: Vec<_> = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(255, 0, 0))) .collect(); // Solid would emit exactly 4 (one per side). Dashed must emit many more. assert!( red_fills.len() > 4, "dashed border should produce multiple segments per side, got {}", red_fills.len() ); }
#[test] fn dotted_border_produces_dot_segments() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0 } div { width: 100px; height: 60px; border: 2px dotted blue; }</style></head><body><div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), ); let list = build_display_list(&tree); let blue_fills: Vec<_> = list .iter() .filter_map(|c| match c { PaintCommand::FillRect { width, height, color, .. } if *color == Color::rgb(0, 0, 255) => Some((*width, *height)), _ => None, }) .collect(); assert!( blue_fills.len() > 4, "dotted border should produce multiple dots per side, got {}", blue_fills.len() ); // Each dot should be square-ish (width == height for top/bottom dots, // or width == thickness for left/right dots) — never a long strip. for (w, h) in &blue_fills { assert!( (*w - 2.0).abs() < 0.001 || (*h - 2.0).abs() < 0.001, "dotted segment should be 2px in at least one axis, got {w}x{h}" ); assert!( *w <= 4.0 && *h <= 4.0, "dotted segment should not be a long strip, got {w}x{h}" ); } }
#[test] fn double_border_produces_two_strips_per_side() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0 } div { width: 100px; height: 60px; border: 9px double green; }</style></head><body><div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), ); let list = build_display_list(&tree); let green_fills: Vec<_> = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(0, 128, 0))) .collect(); // Two strips per side × 4 sides = 8. assert_eq!( green_fills.len(), 8, "double border should produce two strips per side (8 total)" ); }
// --- Visibility painting tests ---
#[test] fn visibility_hidden_not_painted() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }div { visibility: hidden; background-color: red; width: 100px; height: 100px; }</style></head><body><div>Hidden</div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// No red background should be in the display list. let red_fills = list.iter().filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(255, 0, 0)) }); assert_eq!(red_fills.count(), 0, "hidden element should not be painted");
// No text should be rendered for the hidden element. let hidden_text = list.iter().filter( |c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Hidden"), ); assert_eq!( hidden_text.count(), 0, "hidden element text should not be painted" ); }
#[test] fn visibility_visible_child_of_hidden_parent_is_painted() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.parent { visibility: hidden; }.child { visibility: visible; }</style></head><body><div class="parent"><p class="child">Visible</p></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// The visible child's text should appear in the display list. let visible_text = list.iter().filter( |c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text.contains("Visible")), ); assert!( visible_text.count() > 0, "visible child of hidden parent should be painted" ); }
#[test] fn visibility_collapse_not_painted() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }div { visibility: collapse; background-color: blue; width: 50px; height: 50px; }</style></head><body><div>Collapsed</div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let blue_fills = list.iter().filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(0, 0, 255)) }); assert_eq!( blue_fills.count(), 0, "collapse element should not be painted" ); }
// --- Overflow clipping tests ---
#[test] fn overflow_hidden_generates_clip_commands() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: hidden; width: 100px; height: 50px; }</style></head><body><div class="container"><p>Content</p></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let push_count = list .iter() .filter(|c| matches!(c, PaintCommand::PushClip { .. })) .count(); let pop_count = list .iter() .filter(|c| matches!(c, PaintCommand::PopClip)) .count();
assert!(push_count >= 1, "overflow:hidden should emit PushClip"); assert_eq!(push_count, pop_count, "PushClip/PopClip must be balanced");
// A plain rectangular overflow clip carries all-zero corner radii. let rect_clip = list .iter() .find_map(|c| match c { PaintCommand::PushClip { radii, .. } => Some(*radii), _ => None, }) .expect("expected a PushClip"); assert_eq!(rect_clip, [0.0; 4], "rectangular clip must have zero radii"); }
#[test] fn overflow_hidden_with_border_radius_emits_rounded_clip() { // An overflow:hidden container with border-radius must emit a PushClip // whose radii are non-zero so the renderer rounds the clipped corners. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.rounded { overflow: hidden; width: 100px; height: 80px; border-radius: 20px; }</style></head><body><div class="rounded"><p>Clipped</p></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Find the PushClip matching the container's padding box (100x80, no // border so padding box == border box). let radii = list .iter() .find_map(|c| match c { PaintCommand::PushClip { width, height, radii, .. } if (*width - 100.0).abs() < 0.5 && (*height - 80.0).abs() < 0.5 => Some(*radii), _ => None, }) .expect("rounded container must emit a PushClip for its padding box");
assert!( radii.iter().all(|&r| (r - 20.0).abs() < 0.5), "border-radius:20px with no border should yield ~20px clip radii, got {radii:?}" ); }
#[test] fn padding_box_radii_shrink_by_border_width() { // With a border, the padding-box (clip) radius is the border-radius minus // the border width, floored at zero. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.rounded { overflow: hidden; width: 100px; height: 80px; border: 5px solid black; border-radius: 20px; }</style></head><body><div class="rounded"><p>Clipped</p></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Padding box is 100x80 (content+padding), inside the 5px border. let radii = list .iter() .find_map(|c| match c { PaintCommand::PushClip { width, height, radii, .. } if (*width - 100.0).abs() < 0.5 && (*height - 80.0).abs() < 0.5 => Some(*radii), _ => None, }) .expect("rounded bordered container must emit a PushClip");
// 20px outer radius minus 5px border = ~15px inner radius. assert!( radii.iter().all(|&r| (r - 15.0).abs() < 0.5), "expected ~15px inner clip radii, got {radii:?}" ); }
#[test] fn overflow_visible_no_clip_commands() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: visible; width: 100px; height: 50px; }</style></head><body><div class="container"><p>Content</p></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let push_count = list .iter() .filter(|c| matches!(c, PaintCommand::PushClip { .. })) .count();
assert_eq!( push_count, 0, "overflow:visible should not emit clip commands" ); } #[test] fn overflow_hidden_clips_own_text() { // A narrow box with overflow:hidden contains a long unbreakable word // whose natural width far exceeds the content box. The DrawGlyphs // command for the line must be issued after PushClip so per-pixel // clipping in the rasterizer drops the overflowing glyphs. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.box { width: 40px; height: 20px; overflow: hidden; font-size: 16px; }</style></head><body><div class="box">SuperLongUnbreakableWord</div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Locate the PushClip for the .box (padding box at x=0,y=0,w=40,h=20). let push_idx = list.iter().position(|c| { matches!(c, PaintCommand::PushClip { width, height, .. } if (*width - 40.0).abs() < 0.5 && (*height - 20.0).abs() < 0.5) }).expect(".box must emit a PushClip for its padding box");
let pop_idx = list[push_idx..] .iter() .position(|c| matches!(c, PaintCommand::PopClip)) .map(|i| i + push_idx) .expect("matching PopClip must follow");
// The glyph line for the .box's own text content must lie inside the // PushClip/PopClip pair so the rasterizer clips it. let glyph_inside_clip = list[push_idx..pop_idx] .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { .. })); assert!( glyph_inside_clip, "DrawGlyphs for the box's own text must be enclosed by PushClip/PopClip" ); } #[test] fn display_none_not_in_display_list() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.gone { display: none; background-color: green; }</style></head><body><p>Visible</p><div class="gone">Gone</div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let green_fills = list.iter().filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(0, 128, 0)) }); assert_eq!( green_fills.count(), 0, "display:none element should not be in display list" ); }
// --- Overflow scrolling tests ---
#[test] fn overflow_scroll_renders_scrollbar_always() { // overflow:scroll should always render scroll bars, even when content fits. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: scroll; width: 200px; height: 200px; }.child { height: 50px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Should have scroll bar track and thumb FillRect commands. let scrollbar_fills: Vec<_> = list .iter() .filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == SCROLLBAR_TRACK_COLOR || *color == SCROLLBAR_THUMB_COLOR) }) .collect();
assert!( scrollbar_fills.len() >= 2, "overflow:scroll should render track and thumb (got {} fills)", scrollbar_fills.len() ); }
#[test] fn overflow_auto_scrollbar_only_when_overflows() { // overflow:auto with content that fits should NOT show scroll bars. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: auto; width: 200px; height: 200px; }.child { height: 50px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let scrollbar_fills = list.iter().filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == SCROLLBAR_TRACK_COLOR || *color == SCROLLBAR_THUMB_COLOR) });
assert_eq!( scrollbar_fills.count(), 0, "overflow:auto should not render scroll bars when content fits" ); }
#[test] fn overflow_auto_scrollbar_when_content_overflows() { // overflow:auto with content taller than container should show scroll bars. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: auto; width: 200px; height: 100px; }.child { background-color: red; height: 500px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let scrollbar_fills: Vec<_> = list .iter() .filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == SCROLLBAR_TRACK_COLOR || *color == SCROLLBAR_THUMB_COLOR) }) .collect();
assert!( scrollbar_fills.len() >= 2, "overflow:auto should render scroll bars when content overflows (got {} fills)", scrollbar_fills.len() ); } #[test] fn scroll_bar_thumb_proportional() { // Thumb size should be proportional to viewport/content ratio. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: scroll; width: 200px; height: 100px; }.child { height: 400px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Find the thumb FillRect (SCROLLBAR_THUMB_COLOR). let thumb = list.iter().find(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == SCROLLBAR_THUMB_COLOR) });
assert!(thumb.is_some(), "should have a scroll bar thumb"); if let Some(PaintCommand::FillRect { height, .. }) = thumb { // Container height is 100, content is 400. // Thumb ratio = 100/400 = 0.25, track height = 100. // Thumb height = max(0.25 * 100, 20) = 25. assert!( *height >= 20.0 && *height <= 100.0, "thumb height {} should be proportional and within bounds", height ); } } // ----------------------------------------------------------------------- // Sticky positioning paint-time tests // -----------------------------------------------------------------------
#[test] fn sticky_sticks_to_top_when_scrolled() { // A sticky element with top:0 inside a tall container should // stick to the top of the viewport when page-scrolled past it. let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); doc.append_child(root, html); doc.append_child(html, body);
// Container tall enough to scroll. let container = doc.create_element("div"); doc.append_child(body, container); doc.set_attribute(container, "style", "width: 400px; height: 1000px;");
// Spacer pushes sticky down. let spacer = doc.create_element("div"); doc.append_child(container, spacer); doc.set_attribute(spacer, "style", "height: 100px;");
// Sticky element. let sticky = doc.create_element("div"); let text = doc.create_text("Sticky"); doc.append_child(container, sticky); doc.append_child(sticky, text); doc.set_attribute( sticky, "style", "position: sticky; top: 0; height: 30px; background-color: red;", );
let tree = layout_doc(&doc); let scroll_state: ScrollState = HashMap::new();
// With no scroll, the display list should show the sticky at its // normal flow position. let list_no_scroll = build_display_list_with_page_scroll(&tree, 0.0, &scroll_state); // Find the red FillRect — that's our sticky background. let sticky_bg_no_scroll = list_no_scroll .iter() .find(|cmd| matches!(cmd, PaintCommand::FillRect { color, .. } if color.r == 255 && color.g == 0 && color.b == 0)) .expect("should find sticky red background"); let no_scroll_y = match sticky_bg_no_scroll { PaintCommand::FillRect { y, .. } => *y, _ => unreachable!(), }; // Normal position: body has default 8px margin, spacer is 100px, // so sticky should be around y≈108. assert!( no_scroll_y > 90.0, "without scroll, sticky should be at its normal position, got y={}", no_scroll_y, );
// Now scroll 200px — the sticky element's normal position would be // around 108 - 200 = -92 (off screen), but it should stick at y=0. let list_scrolled = build_display_list_with_page_scroll(&tree, 200.0, &scroll_state); let sticky_bg_scrolled = list_scrolled .iter() .find(|cmd| matches!(cmd, PaintCommand::FillRect { color, .. } if color.r == 255 && color.g == 0 && color.b == 0)) .expect("should find sticky red background when scrolled"); let scrolled_y = match sticky_bg_scrolled { PaintCommand::FillRect { y, .. } => *y, _ => unreachable!(), }; // The sticky element should be pinned near y=0 (content box y // accounts for padding/border/margin). assert!( scrolled_y >= -1.0 && scrolled_y < 20.0, "with 200px scroll, sticky should be near top (y≈0), got y={}", scrolled_y, ); }
#[test] fn sticky_constrained_by_parent() { // When the containing block scrolls past, the sticky element should // unstick and scroll away with its container. let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); doc.append_child(root, html); doc.append_child(html, body);
// Small container that will scroll off. let container = doc.create_element("div"); doc.append_child(body, container); doc.set_attribute(container, "style", "width: 400px; height: 200px;");
// Sticky element inside. let sticky = doc.create_element("div"); let text = doc.create_text("Sticky"); doc.append_child(container, sticky); doc.append_child(sticky, text); doc.set_attribute( sticky, "style", "position: sticky; top: 0; height: 30px; background-color: green;", );
// After container — more content so page can scroll further. let after = doc.create_element("div"); doc.append_child(body, after); doc.set_attribute(after, "style", "height: 2000px;");
let tree = layout_doc(&doc); let scroll_state: ScrollState = HashMap::new();
// Scroll far enough that the container is completely off-screen. // Container is at roughly y=8 (body margin) with height 200, // so bottom is at y≈208. Scroll by 400 should move it well off screen. let list = build_display_list_with_page_scroll(&tree, 400.0, &scroll_state); let sticky_bg = list.iter().find(|cmd| { matches!(cmd, PaintCommand::FillRect { color, .. } if color.r == 0 && color.g == 128 && color.b == 0) });
if let Some(PaintCommand::FillRect { y, .. }) = sticky_bg { // The sticky element should have scrolled off with its container. // Its screen y should be negative (off screen). assert!( y < &0.0, "sticky should be off-screen when container scrolled away, got y={}", y, ); } // If not found, the element might not be painted (which is also // acceptable if it's off-screen). }
// ----------------------------------------------------------------------- // Compositing layer tests // -----------------------------------------------------------------------
#[test] fn opacity_element_generates_push_pop_layer() { let html = "<html><body><div style='opacity: 0.5; background: red; width: 100px; height: 50px;'>Hello</div></body></html>"; let doc = we_html::parse_html(html); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// There should be at least one PushLayer with opacity 0.5. let push_count = list .iter() .filter(|cmd| matches!(cmd, PaintCommand::PushLayer { opacity, .. } if *opacity < 1.0)) .count(); assert!(push_count >= 1, "expected PushLayer for opacity < 1.0");
let pop_count = list .iter() .filter(|cmd| matches!(cmd, PaintCommand::PopLayer)) .count(); assert_eq!(push_count, pop_count, "PushLayer/PopLayer must be balanced"); }
#[test] fn full_opacity_no_isolation_layer() { // A fully opaque div should not generate PushLayer with opacity < 1.0. let html = "<html><body><div style='background: blue; width: 100px; height: 50px;'>Hi</div></body></html>"; let doc = we_html::parse_html(html); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let isolated_layers = list .iter() .filter(|cmd| matches!(cmd, PaintCommand::PushLayer { opacity, .. } if *opacity < 1.0)) .count(); assert_eq!( isolated_layers, 0, "opacity=1.0 should not produce isolated layers" ); } #[test] fn needs_compositing_layer_opacity() { use we_style::computed::ComputedStyle; let mut style = ComputedStyle::default(); style.display = we_style::computed::Display::Block; style.opacity = 0.5;
let b = we_layout::LayoutBox::from_style( we_layout::BoxType::Block(NodeId::from_index(0)), &style, ); assert!( needs_compositing_layer(&b), "opacity < 1.0 should need a layer" ); }
#[test] fn needs_compositing_layer_will_change() { use we_style::computed::{ComputedStyle, WillChange}; let mut style = ComputedStyle::default(); style.display = we_style::computed::Display::Block; style.will_change = WillChange { transform: true, opacity: false, };
let b = we_layout::LayoutBox::from_style( we_layout::BoxType::Block(NodeId::from_index(0)), &style, ); assert!( needs_compositing_layer(&b), "will-change: transform should need a layer" ); }
#[test] fn no_layer_for_normal_element() { use we_style::computed::ComputedStyle; let mut style = ComputedStyle::default(); style.display = we_style::computed::Display::Block;
let b = we_layout::LayoutBox::from_style( we_layout::BoxType::Block(NodeId::from_index(0)), &style, ); assert!( !needs_compositing_layer(&b), "normal element should not need a layer" ); } // --- Form control rendering tests ---
#[test] fn text_input_renders_with_border_and_background() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="text" value="hello"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Should have FillRect commands (background + border) for the input. let fill_count = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { .. })) .count(); assert!( fill_count >= 5, "text input should have background + 4 border rects, got {fill_count}" );
// Should have a DrawGlyphs for the value text. let has_value_text = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "hello")); assert!( has_value_text, "text input should render its value text 'hello'" ); }
#[test] fn appearance_none_text_input_skips_native_chrome() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.shell { width: 240px; height: 44px; background: white; border-radius: 22px; }input { -webkit-appearance: none; background-color: transparent; border: 0; width: 180px; height: 44px; padding: 0;}</style></head><body><div class="shell"><input type="search" value=""></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
assert!( list.iter().any(|cmd| matches!( cmd, PaintCommand::RoundedRect { color, .. } if *color == Color::rgb(255, 255, 255) )), "rounded parent background should still paint" ); assert!( !list.iter().any(|cmd| matches!( cmd, PaintCommand::FillRect { color, width, height, .. } if *color == FC_BG_COLOR && *width >= 100.0 && *height >= 30.0 )), "appearance:none text input should not paint native white field chrome" ); }
#[test] fn empty_text_input_renders_placeholder() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="email" placeholder="you@example.com"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let placeholder = list.iter().find_map(|c| match c { PaintCommand::DrawGlyphs { line, .. } if line.text == "you@example.com" => { Some(line.color) } _ => None, }); assert!( matches!(placeholder, Some(FC_PLACEHOLDER_TEXT)), "empty text input should render placeholder text in placeholder color" ); }
#[test] fn empty_password_input_renders_placeholder_not_bullets() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="password" placeholder="Password"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_placeholder = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Password")); let has_bullets = list.iter().any(|c| { matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text.contains('\u{2022}')) }); assert!( has_placeholder, "empty password input should render placeholder text" ); assert!( !has_bullets, "empty password input should not render placeholder as bullets" ); }
#[test] fn password_input_renders_bullets() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="password" value="abc"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Should render bullet characters instead of actual value. let has_bullets = list.iter().any(|c| { matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "\u{2022}\u{2022}\u{2022}") }); assert!( has_bullets, "password input should render bullet characters" );
// Should NOT contain the actual password text. let has_plain = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "abc")); assert!( !has_plain, "password input should not render plaintext value" ); }
#[test] fn checkbox_unchecked_renders_empty_box() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="checkbox"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Unchecked checkbox: background + 4 border rects, no checkmark. let fill_count = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { .. })) .count(); assert!( fill_count >= 5, "unchecked checkbox should have background + borders, got {fill_count}" );
// Should not have any visible text (only placeholder empty fragments are allowed). let has_visible_text = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if !line.text.is_empty())); assert!( !has_visible_text, "unchecked checkbox should not have any visible text glyphs" ); }
#[test] fn checkbox_checked_renders_checkmark() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="checkbox" checked></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Checked checkbox has more FillRects than unchecked (checkmark lines). let fill_count = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { .. })) .count(); assert!( fill_count > 5, "checked checkbox should have extra rects for checkmark, got {fill_count}" ); }
#[test] fn radio_unchecked_renders_circle() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="radio"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Radio button uses scanline circles → many FillRects. let fill_count = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { .. })) .count(); assert!( fill_count >= 5, "radio button should have circle scanline rects, got {fill_count}" ); }
#[test] fn radio_checked_renders_dot() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="radio" checked></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Checked radio has more rects (dot circle). let unchecked_html = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="radio"></body></html>"#; let unchecked_doc = we_html::parse_html(unchecked_html); let unchecked_tree = layout_doc(&unchecked_doc); let unchecked_list = build_display_list(&unchecked_tree);
let checked_fills = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { .. })) .count(); let unchecked_fills = unchecked_list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { .. })) .count(); assert!( checked_fills > unchecked_fills, "checked radio ({checked_fills} fills) should have more rects than unchecked ({unchecked_fills})" ); }
#[test] fn submit_button_renders_label() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="submit" value="Go"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_label = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Go")); assert!(has_label, "submit button should render 'Go' label"); }
#[test] fn submit_button_default_label() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="submit"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_label = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Submit")); assert!( has_label, "submit button without value should render 'Submit'" ); }
#[test] fn zero_sized_submit_button_does_not_render_label() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="submit" style="width: 0; height: 0;"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_label = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Submit")); assert!(!has_label, "zero-sized submit button should not paint text"); }
#[test] fn button_element_renders_label() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><button>Click Me</button></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_label = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Click Me")); assert!(has_label, "button element should render 'Click Me' label"); }
#[test] fn icon_only_button_does_not_render_submit_label() { // A `<button type="submit">` whose only child is a non-text element // (an icon slot) must not synthesize a "Submit" label — regression // guard for blocket.se's icon search button (isu issue 331). let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><button type="submit"><svg width="16" height="16"></svg></button></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_label = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Submit")); assert!( !has_label, "icon-only <button> should not paint a synthesized 'Submit' label" ); }
#[test] fn select_renders_selected_option() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><select> <option>Apple</option> <option selected>Banana</option> <option>Cherry</option></select></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_selected = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Banana")); assert!( has_selected, "select should render the selected option 'Banana'" ); }
#[test] fn select_renders_first_option_when_none_selected() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><select> <option>First</option> <option>Second</option></select></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_first = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "First")); assert!( has_first, "select should render first option 'First' when none selected" ); }
#[test] fn disabled_input_renders_dimmed() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="text" value="dimmed" disabled></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Disabled text should use dimmed color (169, 169, 169). let has_dimmed_text = list.iter().any(|c| { matches!(c, PaintCommand::DrawGlyphs { color, .. } if color.r == 169 && color.g == 169 && color.b == 169) }); assert!( has_dimmed_text, "disabled input should render text with dimmed color" );
// Disabled background should be grayish (235, 235, 228). let has_disabled_bg = list.iter().any(|c| { matches!(c, PaintCommand::FillRect { color, .. } if color.r == 235 && color.g == 235 && color.b == 228) }); assert!( has_disabled_bg, "disabled input should have dimmed background" ); }
#[test] fn textarea_renders_with_value() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><textarea>Some text</textarea></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_text = list.iter().any(|c| { matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text.contains("Some text")) }); assert!(has_text, "textarea should render its text content"); } #[test] fn form_control_info_populated_in_layout() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body> <input type="text" value="test"> <input type="checkbox" checked> <input type="submit" value="Go"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc);
// Find form control boxes. let form_controls: Vec<_> = tree .root .iter() .filter(|b| b.form_control.is_some()) .collect();
assert!( form_controls.len() >= 3, "should have at least 3 form controls in layout, got {}", form_controls.len() );
// Check text input info. let text_input = form_controls.iter().find(|b| { b.form_control .as_ref() .map_or(false, |fc| fc.control_type == FormControlType::TextInput) }); assert!(text_input.is_some(), "should have a TextInput control"); let text_fc = text_input.unwrap().form_control.as_ref().unwrap(); assert_eq!(text_fc.value, "test"); assert!(!text_fc.checked); assert!(!text_fc.disabled);
// Check checkbox info. let checkbox = form_controls.iter().find(|b| { b.form_control .as_ref() .map_or(false, |fc| fc.control_type == FormControlType::Checkbox) }); assert!(checkbox.is_some(), "should have a Checkbox control"); let cb_fc = checkbox.unwrap().form_control.as_ref().unwrap(); assert!(cb_fc.checked);
// Check submit info. let submit = form_controls.iter().find(|b| { b.form_control .as_ref() .map_or(false, |fc| fc.control_type == FormControlType::Submit) }); assert!(submit.is_some(), "should have a Submit control"); let sub_fc = submit.unwrap().form_control.as_ref().unwrap(); assert_eq!(sub_fc.value, "Go"); }
#[test] fn hidden_input_has_no_form_control() { let html_str = r#"<!DOCTYPE html><html><body><input type="hidden" value="secret"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc);
// Hidden inputs should have no form_control info and no replaced_size. let hidden_controls: Vec<_> = tree .root .iter() .filter(|b| b.form_control.is_some()) .collect(); // None of the form controls should be from a hidden input. for b in &hidden_controls { let fc = b.form_control.as_ref().unwrap(); assert_ne!( fc.control_type, FormControlType::TextInput, "hidden input should not produce a form control" ); } }
#[test] fn select_dropdown_arrow_rendered() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><select><option>Item</option></select></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Select should have many FillRects (background + border + arrow scanlines). let fill_count = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { .. })) .count(); assert!( fill_count >= 8, "select should have border + background + arrow rects, got {fill_count}" ); }
#[test] fn reset_button_default_label() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }</style></head><body><input type="reset"></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_label = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Reset")); assert!( has_label, "reset button without value should render 'Reset'" ); }
// --- text-decoration painting ---
/// Returns (x, y, width, fill_color) of every FillRect that sits directly /// after a DrawGlyphs for the given text. These are the decoration strokes. fn decoration_rects_for(list: &DisplayList, text: &str) -> Vec<(f32, f32, f32, Color)> { let mut out = Vec::new(); for window in list.windows(2) { if let ( PaintCommand::DrawGlyphs { line, .. }, PaintCommand::FillRect { x, y, width, height, color, }, ) = (&window[0], &window[1]) { if line.text.contains(text) && *height == 1.0 { out.push((*x, *y, *width, *color)); } } } out }
#[test] fn text_decoration_underline_painted_below_baseline() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; font-size: 20px; }.u { text-decoration: underline; }</style></head><body><p><span class="u">Hello</span></p></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Find the underlined fragment and its decoration rect. let glyph_line = list .iter() .find_map(|c| match c { PaintCommand::DrawGlyphs { line, .. } if line.text.contains("Hello") => Some(line), _ => None, }) .expect("Hello glyphs"); assert_eq!(glyph_line.text_decoration, TextDecoration::Underline);
let rects = decoration_rects_for(&list, "Hello"); assert_eq!(rects.len(), 1, "underline should produce one stroke"); let (_, y, width, _) = rects[0]; let baseline_y = glyph_line.y + glyph_line.baseline_offset; assert!( y > baseline_y, "underline at y={y} must sit below baseline at {baseline_y}" ); assert_eq!(width, glyph_line.width); }
#[test] fn text_decoration_line_through_painted_mid_text() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; font-size: 20px; }.s { text-decoration: line-through; }</style></head><body><p><span class="s">Strike</span></p></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let glyph_line = list .iter() .find_map(|c| match c { PaintCommand::DrawGlyphs { line, .. } if line.text.contains("Strike") => Some(line), _ => None, }) .expect("Strike glyphs"); assert_eq!(glyph_line.text_decoration, TextDecoration::LineThrough);
let rects = decoration_rects_for(&list, "Strike"); assert_eq!(rects.len(), 1, "line-through should produce one stroke"); let (_, y, width, _) = rects[0]; let top = glyph_line.y; let baseline_y = glyph_line.y + glyph_line.baseline_offset; assert!( y > top && y < baseline_y, "line-through at y={y} must sit between top {top} and baseline {baseline_y}" ); assert_eq!(width, glyph_line.width); }
#[test] fn text_decoration_overline_painted_above_text() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; font-size: 20px; }.o { text-decoration: overline; }</style></head><body><p><span class="o">Over</span></p></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let glyph_line = list .iter() .find_map(|c| match c { PaintCommand::DrawGlyphs { line, .. } if line.text.contains("Over") => Some(line), _ => None, }) .expect("Over glyphs"); assert_eq!(glyph_line.text_decoration, TextDecoration::Overline);
let rects = decoration_rects_for(&list, "Over"); assert_eq!(rects.len(), 1, "overline should produce one stroke"); let (_, y, width, _) = rects[0]; let top = glyph_line.y; assert!( (y - top).abs() < 2.0, "overline at y={y} must sit at the top of the text line at {top}" ); assert_eq!(width, glyph_line.width); }
#[test] fn text_decoration_none_paints_no_stroke() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; font-size: 20px; }</style></head><body><p>Plain</p></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let rects = decoration_rects_for(&list, "Plain"); assert!( rects.is_empty(), "undecorated text should produce no 1px decoration stroke, got {rects:?}" ); }
#[test] fn text_decoration_uses_text_color() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; font-size: 20px; }.s { color: #ff0000; text-decoration: line-through; }</style></head><body><p><span class="s">Red</span></p></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let rects = decoration_rects_for(&list, "Red"); assert_eq!(rects.len(), 1); let (_, _, _, color) = rects[0]; assert_eq!(color, Color::rgb(255, 0, 0)); }
#[test] fn fill_rounded_rect_emits_sdf_command() { let mut list = DisplayList::new(); fill_rounded_rect( &mut list, 10.0, 20.0, 100.0, 60.0, [8.0; 4], Color::rgb(1, 2, 3), ); assert_eq!(list.len(), 1); match &list[0] { PaintCommand::RoundedRect { x, y, width, height, radii, stroke_width, color, } => { assert_eq!((*x, *y, *width, *height), (10.0, 20.0, 100.0, 60.0)); assert_eq!(*radii, [8.0; 4]); assert_eq!(*stroke_width, 0.0); assert_eq!(*color, Color::rgb(1, 2, 3)); } other => panic!("expected RoundedRect, got {other:?}"), } }
#[test] fn fill_rounded_rect_with_zero_radii_is_plain_fill() { let mut list = DisplayList::new(); fill_rounded_rect( &mut list, 0.0, 0.0, 50.0, 50.0, [0.0; 4], Color::rgb(9, 9, 9), ); assert_eq!(list.len(), 1); assert!(matches!(list[0], PaintCommand::FillRect { .. })); }
#[test] fn stroke_rounded_rect_emits_ring_with_clamped_radii() { let mut list = DisplayList::new(); // Radii larger than half the box must clamp to min(w, h) / 2 = 15. stroke_rounded_rect( &mut list, 5.0, 5.0, 200.0, 30.0, [40.0; 4], 2.0, Color::rgb(0, 0, 0), ); assert_eq!(list.len(), 1); match &list[0] { PaintCommand::RoundedRect { radii, stroke_width, .. } => { assert_eq!(*radii, [15.0; 4]); assert_eq!(*stroke_width, 2.0); } other => panic!("expected RoundedRect, got {other:?}"), } }}