@@ -64,14 +64,8 @@ var catalog: ?store.Catalog = null;
6464var registry_url_owned : ? []u8 = null ;
6565var first_draw_done = false ;
6666
67- /// Upper/lower split (installed plugins on top, store on bottom) — same shape and autosizing
68- /// behaviour as the Pixi tools pane (`explorer/tools.zig`'s layers/palettes split): the top
69- /// pane's height autofits to its content every frame, and a manual drag becomes the new ceiling
70- /// so autofit never grows back past a size the user deliberately chose. The split ratio itself
71- /// is owned internally by the `PanedWidget` (persisted via `dvui.data`); only the ceiling and
72- /// the previous shown-count (to detect when a refit is needed) are ours to track. See `draw`.
73- var installed_max_split_ratio : f32 = 0.5 ;
74- var prev_installed_shown : usize = 0 ;
67+ const PaneTab = enum { store , installed };
68+ var selected_pane_tab : PaneTab = .store ;
7569
7670var store_scroll_info : dvui.ScrollInfo = .{ .horizontal = .auto };
7771var installed_scroll_info : dvui.ScrollInfo = .{ .horizontal = .auto };
@@ -647,15 +641,15 @@ fn drawCreditLink(src: std.builtin.SourceLocation, text: []const u8, url: ?[]con
647641fn drawDetailTabs () void {
648642 var strip = dvui .box (@src (), .{ .dir = .horizontal }, .{
649643 .expand = .horizontal ,
650- .padding = .{ .x = 12 },
644+ .padding = .{ .x = 12 , . y = 6 , . h = 12 },
651645 });
652646 defer strip .deinit ();
653647
654- drawDetailTab ( @src (), "DETAILS" , 0 , selected_detail_tab == .details );
655- drawDetailTab ( @src (), "CHANGELOG" , 1 , selected_detail_tab == .changelog );
648+ if ( tabButton ( @src (), "DETAILS" , 0 , selected_detail_tab == .details )) selected_detail_tab = .details ;
649+ if ( tabButton ( @src (), "CHANGELOG" , 1 , selected_detail_tab == .changelog )) selected_detail_tab = .changelog ;
656650}
657651
658- fn drawDetailTab (src : std.builtin.SourceLocation , label : []const u8 , id_extra : usize , selected : bool ) void {
652+ fn tabButton (src : std.builtin.SourceLocation , label : []const u8 , id_extra : usize , selected : bool ) bool {
659653 const theme = dvui .themeGet ();
660654
661655 // Sized to the label's own width (no expand), so the underline drawn below — which does
@@ -669,15 +663,11 @@ fn drawDetailTab(src: std.builtin.SourceLocation, label: []const u8, id_extra: u
669663 .id_extra = id_extra ,
670664 .background = false ,
671665 .border = dvui .Rect .all (0 ),
672- .margin = dvui .Rect .all (0 ),
666+ .margin = dvui .Rect .all (2 ),
673667 .padding = .{ .x = 4 , .y = 6 , .w = 4 , .h = 4 },
674668 .color_text = if (selected ) theme .color (.window , .text ) else theme .color (.control , .text ),
675- .font = tab_font .withSize (tab_font .size - 1 ),
669+ .font = tab_font .withSize (tab_font .size ),
676670 });
677- if (clicked ) {
678- selected_detail_tab = if (id_extra == 0 ) .details else .changelog ;
679- }
680-
681671 var underline = dvui .box (@src (), .{}, .{
682672 .id_extra = id_extra ,
683673 .expand = .horizontal ,
@@ -686,6 +676,8 @@ fn drawDetailTab(src: std.builtin.SourceLocation, label: []const u8, id_extra: u
686676 .color_fill = if (selected ) theme .color (.window , .text ) else .transparent ,
687677 });
688678 underline .deinit ();
679+
680+ return clicked ;
689681}
690682
691683/// CHANGELOG's content until real GitHub Releases fetching lands (tracked separately) — an
@@ -1254,8 +1246,8 @@ fn rankEntries(entries: *std.ArrayListUnmanaged(StoreEntry), query: *const fuzzy
12541246
12551247fn draw (_ : ? * anyopaque ) anyerror ! void {
12561248 // Unlike the old flat list, the tab now fills the full explorer viewport height
1257- // (`expand = .both`, not just `.horizontal`) so the upper/lower paned split below gets a
1258- // genuinely bounded height to divide — each pane then scrolls its own overflow (see
1249+ // (`expand = .both`, not just `.horizontal`) so the section below the tab strip gets a
1250+ // genuinely bounded height — it then scrolls its own overflow (see
12591251 // `drawStoreSection`/`drawInstalledSection`) rather than the whole tab growing forever and
12601252 // leaning on the explorer's own scrollArea (`files.zig`'s tree still does that; we don't).
12611253 var vbox = dvui .box (@src (), .{ .dir = .vertical }, .{ .expand = .both });
@@ -1303,7 +1295,7 @@ fn draw(_: ?*anyopaque) anyerror!void {
13031295 const arena = dvui .currentWindow ().arena ();
13041296 const editor = fizzy .editor ;
13051297
1306- // Store entries (upper pane ): one row per registry plugin, independent of local install
1298+ // Store entries (STORE tab ): one row per registry plugin, independent of local install
13071299 // state — a pure "what does the store publish" list. See `drawStoreCard`.
13081300 var store_entries : std .ArrayListUnmanaged (StoreEntry ) = .empty ;
13091301 if (maybe_snapshot ) | snap | {
@@ -1320,7 +1312,7 @@ fn draw(_: ?*anyopaque) anyerror!void {
13201312 }
13211313 rankEntries (& store_entries , & query );
13221314
1323- // Installed entries (lower pane ): everything genuinely present locally — loaded,
1315+ // Installed entries (INSTALLED tab ): everything genuinely present locally — loaded,
13241316 // disabled-on-disk, sideloaded, or a failed/rejected build — enriched with a matching
13251317 // registry row (for "store vX" / Update-availability) wherever the registry knows the id too.
13261318 var installed_entries : std .ArrayListUnmanaged (StoreEntry ) = .empty ;
@@ -1381,55 +1373,36 @@ fn draw(_: ?*anyopaque) anyerror!void {
13811373 }
13821374 rankEntries (& installed_entries , & query );
13831375
1384- // Upper/lower split — identical shape and autosizing behaviour to the Pixi tools pane's
1385- // layers/palettes split (`explorer/tools.zig`): the installed pane autofits snugly to its
1386- // content every frame, up to `installed_max_split_ratio`, unless the sash is actively being
1387- // dragged or an animation is in flight — a manual drag becomes the new ceiling so autofit
1388- // never grows back past a size the user deliberately chose.
1389- var paned = fizzy .dvui .paned (@src (), .{
1390- .direction = .vertical ,
1391- .collapsed_size = 0 ,
1392- .handle_size = 10 ,
1393- // Same reveal distance as every other sash in the app; the default 20 made this one
1394- // appear much later than its neighbours for no reason.
1395- .handle_dynamic = .{ .handle_size_max = 10 , .distance_max = 60 },
1396- }, .{ .expand = .both , .background = false });
1397- defer paned .deinit ();
1398-
1399- if (paned .dragging ) installed_max_split_ratio = paned .split_ratio .* ;
1400-
1401- var shown_installed : usize = 0 ;
1402- if (paned .showFirst ()) {
1403- shown_installed = drawInstalledSection (installed_entries .items , filter_text );
1404- }
1405-
1406- // Must run between `showFirst` and `showSecond` — `getFirstFittedRatio` reads the min size
1407- // the first pane's just-drawn content published.
1408- const autofit = ! paned .dragging and ! paned .animating ;
1409- if (dvui .firstFrame (paned .data ().id ) or prev_installed_shown != shown_installed or autofit ) {
1410- if (dvui .firstFrame (paned .data ().id )) {
1411- // Min sizes for the subtree aren't published yet on the very first frame — so a fit
1412- // computed right now would be wrong. Nudge open (never hard-close to exactly 0):
1413- // `showFirst` below gates whether the installed pane's content runs *at all*, so a
1414- // 0 here would deadlock — the pane could never publish a size to refit from again,
1415- // and only a manual drag of the sash would ever reopen it. Refit properly next frame.
1416- paned .split_ratio .* = 1.0 ;
1417- } else {
1418- const ratio = paned .getFirstFittedRatio (.{
1419- .min_split = 0 ,
1420- .max_split = @min (installed_max_split_ratio , 0.6 ),
1421- .min_size = 0 ,
1422- });
1423- const diff = @abs (ratio - paned .split_ratio .* );
1424- if (diff > 0.000001 ) {
1425- paned .animateSplit (ratio , dvui .easing .outBack );
1426- }
1427- }
1376+ drawPaneTabs (cat .status ());
1377+
1378+ switch (selected_pane_tab ) {
1379+ .store = > _ = drawStoreSection (store_entries .items , filter_text , cat .status ()),
1380+ .installed = > _ = drawInstalledSection (installed_entries .items , filter_text ),
14281381 }
1429- prev_installed_shown = shown_installed ;
1382+ }
1383+
1384+ /// Store / Installed strip below the filter row. Same look as the detail page's
1385+ /// DETAILS/CHANGELOG strip (`drawDetailTabs`), plus the store's refresh spinner pinned to the
1386+ /// right so an in-flight fetch stays visible from either tab.
1387+ fn drawPaneTabs (status : store.Status ) void {
1388+ var strip = dvui .box (@src (), .{ .dir = .horizontal }, .{
1389+ .expand = .horizontal ,
1390+ .padding = .{ .x = 12 , .y = 6 , .h = 6 },
1391+ });
1392+ defer strip .deinit ();
1393+
1394+ if (tabButton (@src (), "STORE" , 0 , selected_pane_tab == .store )) selected_pane_tab = .store ;
1395+ if (tabButton (@src (), "INSTALLED" , 1 , selected_pane_tab == .installed )) selected_pane_tab = .installed ;
14301396
1431- if (paned .showSecond ()) {
1432- _ = drawStoreSection (store_entries .items , filter_text , cat .status ());
1397+ // A refresh over existing cards is a footnote, not a takeover: the cards stay put and this
1398+ // spinner is the only sign a fetch is outstanding.
1399+ if (status == .fetching and have_snapshot ) {
1400+ fizzy .dvui .bubbleSpinner (@src (), .{
1401+ .min_size_content = .{ .w = 14 , .h = 14 },
1402+ .gravity_x = 1.0 ,
1403+ .gravity_y = 0.5 ,
1404+ .color_text = dvui .themeGet ().color (.window , .text ).opacity (0.7 ),
1405+ }, .{});
14331406 }
14341407}
14351408
@@ -1536,31 +1509,15 @@ fn cardAnimator(src: std.builtin.SourceLocation, entry: StoreEntry, index: usize
15361509 });
15371510}
15381511
1539- /// Lower pane : a pure "browse the store" list, one card per registry plugin. Wrapped in its
1512+ /// STORE tab : a pure "browse the store" list, one card per registry plugin. Wrapped in its
15401513/// own scrollArea (independent of the installed pane above) with the same edge-shadow treatment
1541- /// `explorer/Explorer.zig` uses, so a manually-shrunk pane or an overly-wide card still scrolls
1542- /// with the usual visual hint instead of clipping silently. Returns the shown count (unused by
1543- /// the caller now that the store pane no longer drives the paned autofit).
1514+ /// `explorer/Explorer.zig` uses, so a narrow pane or an overly-wide card still scrolls with the
1515+ /// usual visual hint instead of clipping silently. Returns the shown count (unused).
15441516///
15451517/// This pane — and only this pane — owns the registry's loading/offline states: the spinner while
15461518/// the first fetch is in flight, the unreachable empty state when it fails, and a small inline
15471519/// spinner beside the header while a refresh runs over already-shown cards.
15481520fn drawStoreSection (entries : []const StoreEntry , filter_text : []const u8 , status : store.Status ) usize {
1549- {
1550- var header = dvui .box (@src (), .{ .dir = .horizontal }, .{ .expand = .horizontal });
1551- defer header .deinit ();
1552- dvui .labelNoFmt (@src (), "STORE" , .{}, .{ .font = dvui .Font .theme (.heading ), .margin = .{ .x = 8 } });
1553- // A refresh over existing cards is a footnote, not a takeover: the cards stay put and this
1554- // spinner is the only sign a fetch is outstanding.
1555- if (status == .fetching and have_snapshot ) {
1556- fizzy .dvui .bubbleSpinner (@src (), .{
1557- .min_size_content = .{ .w = 14 , .h = 14 },
1558- .gravity_y = 0.5 ,
1559- .color_text = dvui .themeGet ().color (.window , .text ).opacity (0.7 ),
1560- }, .{});
1561- }
1562- }
1563-
15641521 var pane_box = dvui .box (@src (), .{ .dir = .vertical }, .{ .expand = .both , .background = false });
15651522 defer pane_box .deinit ();
15661523
@@ -1605,13 +1562,11 @@ fn drawStoreSection(entries: []const StoreEntry, filter_text: []const u8, status
16051562 return shown ;
16061563}
16071564
1608- /// Upper pane : everything genuinely present locally, grouped under "Local" (sideloaded dylibs)
1565+ /// INSTALLED tab : everything genuinely present locally, grouped under "Local" (sideloaded dylibs)
16091566/// and "Built-in" (bundled + static built-ins) headers. This is the *only* place enable/disable,
16101567/// update, uninstall, and failed-to-load detail show up — see `drawCard`. Returns the shown
1611- /// count (drives the paned autofit refit trigger in `draw` ).
1568+ /// count (unused ).
16121569fn drawInstalledSection (entries : []const StoreEntry , filter_text : []const u8 ) usize {
1613- dvui .labelNoFmt (@src (), "INSTALLED" , .{}, .{ .font = dvui .Font .theme (.heading ), .margin = .{ .y = 4 } });
1614-
16151570 var pane_box = dvui .box (@src (), .{ .dir = .vertical }, .{ .expand = .both , .background = false });
16161571 defer pane_box .deinit ();
16171572
@@ -1662,7 +1617,7 @@ fn drawInstalledSection(entries: []const StoreEntry, filter_text: []const u8) us
16621617 return shown ;
16631618}
16641619
1665- /// Small uppercase-ish section label above a group of cards in the upper pane . `id_extra`
1620+ /// Small uppercase-ish section label above a group of cards in the INSTALLED tab . `id_extra`
16661621/// disambiguates the "Local" and "Built-in" calls, which otherwise share a source location.
16671622fn drawSectionHeader (title : []const u8 , id_extra : usize ) void {
16681623 dvui .labelNoFmt (@src (), title , .{}, .{
@@ -1678,15 +1633,15 @@ fn isBuiltIn(id: []const u8) bool {
16781633 return isBundled (id );
16791634}
16801635
1681- /// Lower-pane card: full state — enabled checkbox, update/uninstall, failed-to-load detail —
1636+ /// INSTALLED-tab card: full state — enabled checkbox, update/uninstall, failed-to-load detail —
16821637/// via `drawCardControls`/`infoLine` (which still shows "installed vX"). See `drawCardShell`.
16831638fn drawCard (entry : StoreEntry ) void {
16841639 var buf : [192 ]u8 = undefined ;
16851640 drawCardShell (entry , drawCardControls , infoLine (& buf , entry ), true );
16861641}
16871642
1688- /// Upper-pane card: browse-only — just an install button or a "no compatible build" message via
1689- /// `drawStoreCardControls`/`storeInfoLine` (never "installed vX": that's the lower pane 's job,
1643+ /// STORE-tab card: browse-only — just an install button or a "no compatible build" message via
1644+ /// `drawStoreCardControls`/`storeInfoLine` (never "installed vX": that's the INSTALLED tab 's job,
16901645/// even for a store plugin the user happens to already have installed). See `drawCardShell`.
16911646fn drawStoreCard (entry : StoreEntry ) void {
16921647 var buf : [192 ]u8 = undefined ;
@@ -2010,7 +1965,7 @@ fn infoLine(buf: []u8, entry: StoreEntry) []const u8 {
20101965}
20111966
20121967/// Compose the dim `id · store v{latest}` line for a store (upper-pane) card into `buf`. Unlike
2013- /// `infoLine`, this never shows install state — the upper pane is a pure "what does the store
1968+ /// `infoLine`, this never shows install state — the STORE tab is a pure "what does the store
20141969/// publish" list, even for a plugin the user happens to already have installed (see `drawCard`).
20151970fn storeInfoLine (buf : []u8 , entry : StoreEntry ) []const u8 {
20161971 var latest_buf : [40 ]u8 = undefined ;
0 commit comments