[{"data":1,"prerenderedAt":803},["ShallowReactive",2],{"devlog-\u002Fdevlog\u002F02-building-with-gpui":3},{"id":4,"title":5,"author":6,"body":9,"date":794,"description":111,"extension":795,"meta":796,"navigation":384,"path":797,"seo":798,"stem":799,"summary":16,"tags":800,"__hash__":802},"devlog\u002Fdevlog\u002F02-building-with-gpui.md","Building with GPUI",{"name":7,"github":8},"Viktor","hystericca",{"type":10,"value":11,"toc":790},"minimark",[12,17,29,40,65,71,78,89,93,99,102,112,124,131,197,216,231,237,240,771,786],[13,14,16],"h2",{"id":15},"the-weirdest-way-to-learn-rust","The weirdest way to learn Rust",[18,19,20,21,24,25],"p",{},"As I mentioned in my previous post, ",[22,23],"ophelia",{}," was originally going to use Tauri as its framework.\nBut while looking for good projects to read source code from, I had a thought: ",[26,27,28],"em",{},"\"Wait, my IDE (Zed) is\nbuilt entirely in Rust with a proprietary UI library.\"",[18,30,31,32,39],{},"I actually tried contributing to Zed before! As you can see in ",[33,34,38],"a",{"href":35,"rel":36},"https:\u002F\u002Fgithub.com\u002Fzed-industries\u002Fzed\u002Fpull\u002F51907",[37],"nofollow","this PR"," — which will hopefully get merged, or make me look like a dumbass.",[18,41,42,43,48,49,53,54,57,58,57,61,64],{},"The reason I never thought of using GPUI in the first place was because I was (reasonably) scared of\nbuilding a UI in a low-level library. If you've ever worked with SDL, GTK, or Qt from C or C++, you\nknow how verbose and tedious they are. But after reading through ",[33,44,47],{"href":45,"rel":46},"https:\u002F\u002Fgithub.com\u002Fzed-industries\u002Fzed\u002Ftree\u002Fmain\u002Fcrates\u002Fgpui",[37],"the GPUI source",",\nI was surprised to find that the API is modeled almost exactly after Tailwind. Layout is done with ",[50,51,52],"code",{},".flex()",",\n",[50,55,56],{},".items_center()",", ",[50,59,60],{},".gap_3()",[50,62,63],{},".p_4()",", so just underscores instead of dashes. And you know who\nuses Tailwind? Yours truly. The original Tauri frontend was built with it.",[18,66,67,68],{},"So I started rebuilding the UI from scratch in GPUI. ",[26,69,70],{},"Wow, this backend will never get built, huh?",[18,72,73],{},[74,75],"img",{"alt":76,"src":77},"GUI Schema","\u002Fgui-schema.png",[18,79,80,81,84,85,88],{},"The other thing I noticed is that GPUI uses a component model similar to Vue. You have stateful views\n(",[50,82,83],{},"Render",") that hold data and get re-rendered when state changes, and stateless components\n(",[50,86,87],{},"RenderOnce",") that are just functions from data to UI elements. If you've used Vue or React,\nthe mental model transfers pretty cleanly.",[13,90,92],{"id":91},"drawing-the-logo","Drawing the logo",[18,94,95,96,98],{},"I also wanted to render the ",[22,97],{}," logo natively in GPUI instead of loading an SVG file. partly\nfor interactivity (hover states, animations tied to download state), partly because I enjoy\nunnecessary detours.",[18,100,101],{},"The logo is two off-center circles with an evenodd fill, plus a small dot:",[103,104,109],"pre",{"className":105,"code":107,"language":108},[106],"language-text","Outer ring:  center (12, 12)  r = 9\nInner cutout: center (15, 12)  r = 7.5   ← shifted 3px right\nDot:          center (15, 12)  r = 1.5\n","text",[50,110,107],{"__ignoreMap":111},"",[18,113,114,115,118,119,123],{},"The offset is what makes it interesting. Because the inner circle's center is shifted right, the\nring is thicker on the left and thinner on the right. And because ",[50,116,117],{},"r_inner + d > r_outer","\n(7.5 + 3 = 10.5 > 9), the inner circle actually extends ",[120,121,122],"strong",{},"1.5 units past the outer circle"," on the\nright (to x = 22.5 vs x = 21). With the evenodd fill rule, that overhang gets filled it's the\nlittle crescent that sticks out on the right side of the logo (it looks a lil ominous I know)",[18,125,126,127,130],{},"The evenodd rule is simple: a point is filled if it's inside an ",[120,128,129],{},"odd"," number of shapes. So:",[132,133,134,150],"table",{},[135,136,137],"thead",{},[138,139,140,144,147],"tr",{},[141,142,143],"th",{},"Region",[141,145,146],{},"Inside shapes",[141,148,149],{},"Filled?",[151,152,153,165,176,187],"tbody",{},[138,154,155,159,162],{},[156,157,158],"td",{},"Outside both",[156,160,161],{},"0",[156,163,164],{},"no",[138,166,167,170,173],{},[156,168,169],{},"Inside outer only",[156,171,172],{},"1",[156,174,175],{},"yes (the ring)",[138,177,178,181,184],{},[156,179,180],{},"Inside both",[156,182,183],{},"2",[156,185,186],{},"no (the hole)",[138,188,189,192,194],{},[156,190,191],{},"Inside inner only (right overhang)",[156,193,172],{},[156,195,196],{},"yes (the crescent)",[18,198,199,200,203,204,207,208,211,212,215],{},"My first attempt used GPUI's ",[50,201,202],{},"Path"," API directly, which only has ",[50,205,206],{},"curve_to",", a quadratic bezier.\nQuadratic bezier circles are a known approximation: you split the circle into 4 arcs of 90° each and\nuse the corner of the bounding box as the control point. The midpoint of each arc ends up at distance\n",[50,209,210],{},"r√(0.75² + 0.75²) ≈ 1.06r"," from the center instead of ",[50,213,214],{},"r",", so you get about 6% outward bulge per\nsegment. Noticeable at small sizes, and it made the evenodd winding trick unreliable.",[18,217,218,219,222,223,226,227,230],{},"The fix was ",[50,220,221],{},"PathBuilder",", which wraps Lyon's SVG path tessellator and has proper ",[50,224,225],{},"arc_to"," support\nand ",[50,228,229],{},"FillRule::EvenOdd",". The SVG path for the ring is:",[103,232,235],{"className":233,"code":234,"language":108},[106],"M12 3 A9 9 0 1 0 12 21 A9 9 0 1 0 12 3 Z\nM15 4.5 A7.5 7.5 0 1 0 15 19.5 A7.5 7.5 0 1 0 15 4.5 Z\n",[50,236,234],{"__ignoreMap":111},[18,238,239],{},"And the GPUI translation is nearly identical; two subpaths, each drawn as a pair of 180° arcs,\nwith the fill rule set to evenodd before tessellation:",[103,241,245],{"className":242,"code":243,"language":244,"meta":111,"style":111},"language-rust shiki shiki-themes github-dark-default","let mut builder = PathBuilder::fill().with_style(PathStyle::Fill(\n    FillOptions::default().with_fill_rule(FillRule::EvenOdd),\n));\nbuilder.scale(scale);\nbuilder.translate(point(px(ox), px(oy)));\n\n\u002F\u002F outer circle\nbuilder.move_to(point(px(21.0), px(12.0)));\nbuilder.arc_to(point(px(9.0), px(9.0)), px(0.0), false, false, point(px(3.0), px(12.0)));\nbuilder.arc_to(point(px(9.0), px(9.0)), px(0.0), false, false, point(px(21.0), px(12.0)));\nbuilder.close();\n\n\u002F\u002F inner circle (offset right and overlap becomes hole, overhang becomes crescent)\nbuilder.move_to(point(px(22.5), px(12.0)));\nbuilder.arc_to(point(px(7.5), px(7.5)), px(0.0), false, false, point(px(7.5), px(12.0)));\nbuilder.arc_to(point(px(7.5), px(7.5)), px(0.0), false, false, point(px(22.5), px(12.0)));\nbuilder.close();\n","rust",[50,246,247,300,331,337,351,379,386,393,430,502,569,582,587,593,625,693,760],{"__ignoreMap":111},[248,249,252,256,259,263,266,270,273,277,280,283,286,289,292,294,297],"span",{"class":250,"line":251},"line",1,[248,253,255],{"class":254},"suJrU","let",[248,257,258],{"class":254}," mut",[248,260,262],{"class":261},"sZEs4"," builder ",[248,264,265],{"class":254},"=",[248,267,269],{"class":268},"sQhOw"," PathBuilder",[248,271,272],{"class":254},"::",[248,274,276],{"class":275},"sc3cj","fill",[248,278,279],{"class":261},"()",[248,281,282],{"class":254},".",[248,284,285],{"class":275},"with_style",[248,287,288],{"class":261},"(",[248,290,291],{"class":268},"PathStyle",[248,293,272],{"class":254},[248,295,296],{"class":275},"Fill",[248,298,299],{"class":261},"(\n",[248,301,303,306,308,311,313,315,318,320,323,325,328],{"class":250,"line":302},2,[248,304,305],{"class":268},"    FillOptions",[248,307,272],{"class":254},[248,309,310],{"class":275},"default",[248,312,279],{"class":261},[248,314,282],{"class":254},[248,316,317],{"class":275},"with_fill_rule",[248,319,288],{"class":261},[248,321,322],{"class":268},"FillRule",[248,324,272],{"class":254},[248,326,327],{"class":268},"EvenOdd",[248,329,330],{"class":261},"),\n",[248,332,334],{"class":250,"line":333},3,[248,335,336],{"class":261},"));\n",[248,338,340,343,345,348],{"class":250,"line":339},4,[248,341,342],{"class":261},"builder",[248,344,282],{"class":254},[248,346,347],{"class":275},"scale",[248,349,350],{"class":261},"(scale);\n",[248,352,354,356,358,361,363,366,368,371,374,376],{"class":250,"line":353},5,[248,355,342],{"class":261},[248,357,282],{"class":254},[248,359,360],{"class":275},"translate",[248,362,288],{"class":261},[248,364,365],{"class":275},"point",[248,367,288],{"class":261},[248,369,370],{"class":275},"px",[248,372,373],{"class":261},"(ox), ",[248,375,370],{"class":275},[248,377,378],{"class":261},"(oy)));\n",[248,380,382],{"class":250,"line":381},6,[248,383,385],{"emptyLinePlaceholder":384},true,"\n",[248,387,389],{"class":250,"line":388},7,[248,390,392],{"class":391},"sH3jZ","\u002F\u002F outer circle\n",[248,394,396,398,400,403,405,407,409,411,413,417,420,422,424,427],{"class":250,"line":395},8,[248,397,342],{"class":261},[248,399,282],{"class":254},[248,401,402],{"class":275},"move_to",[248,404,288],{"class":261},[248,406,365],{"class":275},[248,408,288],{"class":261},[248,410,370],{"class":275},[248,412,288],{"class":261},[248,414,416],{"class":415},"sFSAA","21.0",[248,418,419],{"class":261},"), ",[248,421,370],{"class":275},[248,423,288],{"class":261},[248,425,426],{"class":415},"12.0",[248,428,429],{"class":261},")));\n",[248,431,433,435,437,439,441,443,445,447,449,452,454,456,458,460,463,465,467,470,472,475,477,479,481,483,485,487,489,492,494,496,498,500],{"class":250,"line":432},9,[248,434,342],{"class":261},[248,436,282],{"class":254},[248,438,225],{"class":275},[248,440,288],{"class":261},[248,442,365],{"class":275},[248,444,288],{"class":261},[248,446,370],{"class":275},[248,448,288],{"class":261},[248,450,451],{"class":415},"9.0",[248,453,419],{"class":261},[248,455,370],{"class":275},[248,457,288],{"class":261},[248,459,451],{"class":415},[248,461,462],{"class":261},")), ",[248,464,370],{"class":275},[248,466,288],{"class":261},[248,468,469],{"class":415},"0.0",[248,471,419],{"class":261},[248,473,474],{"class":415},"false",[248,476,57],{"class":261},[248,478,474],{"class":415},[248,480,57],{"class":261},[248,482,365],{"class":275},[248,484,288],{"class":261},[248,486,370],{"class":275},[248,488,288],{"class":261},[248,490,491],{"class":415},"3.0",[248,493,419],{"class":261},[248,495,370],{"class":275},[248,497,288],{"class":261},[248,499,426],{"class":415},[248,501,429],{"class":261},[248,503,505,507,509,511,513,515,517,519,521,523,525,527,529,531,533,535,537,539,541,543,545,547,549,551,553,555,557,559,561,563,565,567],{"class":250,"line":504},10,[248,506,342],{"class":261},[248,508,282],{"class":254},[248,510,225],{"class":275},[248,512,288],{"class":261},[248,514,365],{"class":275},[248,516,288],{"class":261},[248,518,370],{"class":275},[248,520,288],{"class":261},[248,522,451],{"class":415},[248,524,419],{"class":261},[248,526,370],{"class":275},[248,528,288],{"class":261},[248,530,451],{"class":415},[248,532,462],{"class":261},[248,534,370],{"class":275},[248,536,288],{"class":261},[248,538,469],{"class":415},[248,540,419],{"class":261},[248,542,474],{"class":415},[248,544,57],{"class":261},[248,546,474],{"class":415},[248,548,57],{"class":261},[248,550,365],{"class":275},[248,552,288],{"class":261},[248,554,370],{"class":275},[248,556,288],{"class":261},[248,558,416],{"class":415},[248,560,419],{"class":261},[248,562,370],{"class":275},[248,564,288],{"class":261},[248,566,426],{"class":415},[248,568,429],{"class":261},[248,570,572,574,576,579],{"class":250,"line":571},11,[248,573,342],{"class":261},[248,575,282],{"class":254},[248,577,578],{"class":275},"close",[248,580,581],{"class":261},"();\n",[248,583,585],{"class":250,"line":584},12,[248,586,385],{"emptyLinePlaceholder":384},[248,588,590],{"class":250,"line":589},13,[248,591,592],{"class":391},"\u002F\u002F inner circle (offset right and overlap becomes hole, overhang becomes crescent)\n",[248,594,596,598,600,602,604,606,608,610,612,615,617,619,621,623],{"class":250,"line":595},14,[248,597,342],{"class":261},[248,599,282],{"class":254},[248,601,402],{"class":275},[248,603,288],{"class":261},[248,605,365],{"class":275},[248,607,288],{"class":261},[248,609,370],{"class":275},[248,611,288],{"class":261},[248,613,614],{"class":415},"22.5",[248,616,419],{"class":261},[248,618,370],{"class":275},[248,620,288],{"class":261},[248,622,426],{"class":415},[248,624,429],{"class":261},[248,626,628,630,632,634,636,638,640,642,644,647,649,651,653,655,657,659,661,663,665,667,669,671,673,675,677,679,681,683,685,687,689,691],{"class":250,"line":627},15,[248,629,342],{"class":261},[248,631,282],{"class":254},[248,633,225],{"class":275},[248,635,288],{"class":261},[248,637,365],{"class":275},[248,639,288],{"class":261},[248,641,370],{"class":275},[248,643,288],{"class":261},[248,645,646],{"class":415},"7.5",[248,648,419],{"class":261},[248,650,370],{"class":275},[248,652,288],{"class":261},[248,654,646],{"class":415},[248,656,462],{"class":261},[248,658,370],{"class":275},[248,660,288],{"class":261},[248,662,469],{"class":415},[248,664,419],{"class":261},[248,666,474],{"class":415},[248,668,57],{"class":261},[248,670,474],{"class":415},[248,672,57],{"class":261},[248,674,365],{"class":275},[248,676,288],{"class":261},[248,678,370],{"class":275},[248,680,288],{"class":261},[248,682,646],{"class":415},[248,684,419],{"class":261},[248,686,370],{"class":275},[248,688,288],{"class":261},[248,690,426],{"class":415},[248,692,429],{"class":261},[248,694,696,698,700,702,704,706,708,710,712,714,716,718,720,722,724,726,728,730,732,734,736,738,740,742,744,746,748,750,752,754,756,758],{"class":250,"line":695},16,[248,697,342],{"class":261},[248,699,282],{"class":254},[248,701,225],{"class":275},[248,703,288],{"class":261},[248,705,365],{"class":275},[248,707,288],{"class":261},[248,709,370],{"class":275},[248,711,288],{"class":261},[248,713,646],{"class":415},[248,715,419],{"class":261},[248,717,370],{"class":275},[248,719,288],{"class":261},[248,721,646],{"class":415},[248,723,462],{"class":261},[248,725,370],{"class":275},[248,727,288],{"class":261},[248,729,469],{"class":415},[248,731,419],{"class":261},[248,733,474],{"class":415},[248,735,57],{"class":261},[248,737,474],{"class":415},[248,739,57],{"class":261},[248,741,365],{"class":275},[248,743,288],{"class":261},[248,745,370],{"class":275},[248,747,288],{"class":261},[248,749,614],{"class":415},[248,751,419],{"class":261},[248,753,370],{"class":275},[248,755,288],{"class":261},[248,757,426],{"class":415},[248,759,429],{"class":261},[248,761,763,765,767,769],{"class":250,"line":762},17,[248,764,342],{"class":261},[248,766,282],{"class":254},[248,768,578],{"class":275},[248,770,581],{"class":261},[18,772,773,775,776,779,780,782,783,785],{},[50,774,221],{}," tessellates this into a triangle mesh at build\ntime, with the evenodd rule baked in.\nBy the time ",[50,777,778],{},"window.paint_path()"," sees it, it's just triangles, so\nno fill rule evaluation at render time.\nThe ",[50,781,347],{}," + ",[50,784,360],{}," transform is applied during\ntessellation too, so the coordinates stay in the clean\n24×24 SVG space until the last moment.",[787,788,789],"style",{},"html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":111,"searchDepth":302,"depth":302,"links":791},[792,793],{"id":15,"depth":302,"text":16},{"id":91,"depth":302,"text":92},"2026-03-23","md",{},"\u002Fdevlog\u002F02-building-with-gpui",{"title":5,"description":111},"devlog\u002F02-building-with-gpui",[801],"update","tIIF_6ni3QKkq4ihetYw-43osgZ3VSQ64bt0GAGt5BE",1777630297685]