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