Conversation
A treemap that draws every file it can subdivide is a mosaic: on a real drive most of the pixels are specks too small to identify, and finding out what any of them is costs a hover, one block at a time. That is the whole of the "visual overload" complaint, and no depth setting fixes it — the setting the issue asked for decides how much of the tree is drawn, not whether what is drawn can be read. Three changes, all in the layout: Culling is by side, not area. `min_area_px: 3.0` accepted a 40×0.1 sliver because 4 px² cleared the floor; `min_side_px` requires both dimensions, so what survives is something a person can actually see. Culled children keep eating their share of the parent, as before — that contract is what makes the parent's area honest, and the test that pinned it still does. A directory reserves a strip at the top of its interior for its label, and its children tile what is left. The strip is taken from the *children's* frame and never from the directory's own rect: that rect has to stay a function of its own frame alone, or a depth cap that stops recursion early would shift the rects that survive it, and the UI switches depth live. The test that pins that prefix property runs with a strip on. Depth became adaptive. A directory subdivides only while its interior can still hold a legible child — padding, strip and padding again come out of every level, so a branch runs out of room and stops on its own. Big folders go deep because they have the pixels; small ones stay a plate. max_depth is now only an upper bound on top of that, which leaves room for the explicit depth preference to land on top of it without fighting it. The rect carries the text too. Names come from `tree.name(id)` at the DTO boundary rather than through `TreemapRect`, which stays `Copy` and geometry-only for the layout's own tests; one string per visible rect is also far cheaper than the round trip per node it would otherwise take to label a block.
The canvas drew coloured rectangles and nothing else, so the map answered "where did the space go" and only answered "what is this" through a tooltip, one block at a time. Now every block that can hold a line of text carries its name, and the sizes ride along: folded into a directory's label at the top of its plate, centred under a file's name in the block itself. This is what SpaceSniffer does, and it is why its map reads at a glance instead of needing to be interrogated. Nesting is drawn as a shade rather than a texture. The old grain said "there is something in here too small to draw", which is accurate and also impossible to tell apart from a block that is simply empty; a fill that steps away from the theme's plate colour once per level instead says how deep you are, on the plate itself, and reads the same light or dark — the step flips direction with the theme. The grain tile and the `--color-plate-grain` token it needed are gone. Text picks its own ink: file blocks keep fixed category colours whatever the theme, so choosing a theme token for the text under them would be choosing it for the wrong surface. Blocks too short to hold their label's strip are left unlabelled rather than drawn half outside themselves — with a six-pixel legibility floor those are the plates the layout already stopped subdividing, and a label hanging out of its own block reads as a label for whatever is beneath it.
Reverts the look from the previous two commits. The shade ramp, the names written into every block and the strip reserved for them borrowed more from SpaceSniffer than this app wants: mathom's treemap is a map of coloured blocks and that is the thing worth keeping. What survives is the part that was never about looks — the legibility floor and the adaptive subdivision, which decide how much gets drawn rather than how it is painted. Naming the blocks is still worth having, so it moves behind Settings → View as "Show names in treemap", off by default like the extra chrome it is. Off, the layout reserves no strip at all (`label_px` is zero), so the geometry is exactly what it was before labels existed. On, the label strip is the one part of a plate that carries no grain — a name read through the texture is a name you have to squint at — and the plates are otherwise the flat colour and dots they have always been.
`min_side_px` was read as a cut-off line: a child under it was skipped, but its share of the frame stayed reserved, so a directory that was mostly small files came out as a patch of flat nothing. SpaceSniffer does the opposite, and it is the right way round — the small stuff is drawn at one uniform minimum size so the directory tiles edge to edge, and only when even that does not fit does the smallest go. Nobody opens this thing to look at 4KB files anyway. So the layout now floors every child at `min_side²`, sheds the tail until the floored weights fit the frame, and re-normalizes the survivors into exactly what is left. Dropping is what pays for the floor, and because the survivors renormalize, the space the dropped ones held flows into the blocks that remain rather than opening a hole. A row can still come out too thin to read — one file holding 99.985% of a block squeezes the rest into a 0.09px column, which hit-testing wins for the whole height of it, since it walks deepest-first. Laying out into a scratch row list before emitting anything lets the fit check the result and shed from the first row that gives out. `keep` only ever shrinks and a lone block fills the frame, so it terminates. The rendering side is untouched: still flat plates and grain, labels still behind the setting. Measured on /usr/share (337k nodes): 1826 rects in 4ms, where strictly-proportional layout gives 86754.
Two things the last round got wrong, both visible. **Folding folders nested into meshes.** A directory subdivided as long as its interior could hold one minimum block, so a folder of packages became a folder of packages of specks — every level spending padding to cut the next one finer, until the map was a field of 6px blocks nobody could read or point at. A level is now taken only when the *biggest* child would land well clear of the floor (twice it, on a side). A folder whose best child is still a speck says more as one plate. Measured on /usr/share (337k nodes): 3686 rects, down from 6969 with the gate off. **Labels moved the blocks.** Turning the mode on reserved a strip at the top of every directory for its name, so the geometry with labels on was not the geometry with them off. `label_px` is gone from the layout entirely and `get_treemap` no longer takes a `labels` flag: there is now no way for the geometry to know, so toggling is a repaint rather than a refetch. Names are painted into blocks that have room for them, and only into *solid* ones — a block the layout subdivided is covered by its children, and a name centred in it would land on top of them. Which leaves the seam. A solid folder plate had nothing to say where it ended: the grain under it is one continuous pattern and the parent's plate fills the gaps, so thirty equal folders drew as a single flat field — "show one folder" showed nothing. Solid plates now get the 1px seam the file blocks always had, painted in the background colour. Plates that have children get nothing, or every subdivided directory would ring itself in a hairline frame. Shedding is proportional now rather than "drop everything the failing row was holding". A row is fit or unfit only as part of a whole layout — the same run of equal blocks tiles legibly at some counts and not others — so stopping at the first failure left a directory of equals as one plate. A slice at a time converges from any starting point and drops the smallest first, which is the order this was always meant to drop in.
Two additions to what decides whether a directory becomes a plate. A directory with more than 200 direct children is one now, by default. At that width its blocks stop being things you can point at and become a texture, and a plate you can click says more. Counted from the children that would actually be drawn, not from `tree.children`, so a filter or hide_system can bring a directory back under the limit — and counted before the sort, so the directories this is meant to keep off the map never pay to order their children. `layout_with_force` takes one id and opens it, plus every ancestor between it and the layout root. One value carries the whole accordion: asking for a directory outside the open one leaves the old one off the chain, so it closes; asking for one inside keeps it on, so it stays. Where the override is consulted matters more than what it does. Both gates return before a single child is placed, so it never reaches `items`, `scale`, `fit` or a frame — opening a directory adds rects under it and moves nothing. A forced layout is a *superset* of the unforced one, which is what lets the map expand in place without a jump, and it is pinned as that rather than as the weaker prefix property. The recursion bound is deliberately not lifted: it is the only brake on a tree whose depth is unbounded, and a click that lands 24 levels down should not be what removes it. The two fixtures that trip the new limit were resized to stay under it — their subject is the legibility rules, and 210 and 640 children were incidental to that.
Three things that only made sense together. A folder plate was the app's own background grey, so a folder the layout stopped short of subdividing read as nothing there at all — which made "show one folder" show an absence. Plates are now `#3b4a6b`, the one dark category colour and the only one that is not a file type, so a container is never mistaken for its contents. It does not move with the theme, which is why the seam between two plates is now a shade of the plate rather than the app background: that painted a white hairline through the map in the light theme. `--color-plate` and `--color-plate-grain` are gone, both sides in one commit — a `ctx.fillStyle = ""` left behind by a deleted variable is silently ignored rather than an error. Single click on a plate opens it in place, through `layout_with_force`. Everything else clicks as it did: a subdivided folder still zooms, a file still only selects. The double click that used to do nothing is now what zooms a plate, so the click has to wait out the double-click window — and only that click. A click on a plate is deferred behind a 300ms timer; a click on anything else acts on the spot. And when opening in place would show nothing — the biggest thing inside still under twice the minimum block — the folder is zoomed instead, with the override kept so it arrives open rather than as one big plate. That is the line the layout itself draws between "worth a level" and "not", so it is the same judgement, applied to an ask that outranks it.
I read the ask as "give folders a colour" and stopped there, so a folder kept the dot texture files never had. That made a plate read as a surface full of specks rather than as a block — the opposite of the point, which was for a folder to look like a file with a different colour and nothing else. So: one flat fill, the same 1% gap, the same sheen, same everything. `FOLDER_PLATE` and the highlight sprite are the whole of it; the grain tile, its cache and the seam pass are gone, since a 1px gap against the backdrop does what the seam was invented to do. A directory that *did* subdivide is no longer painted at all — it is only the backdrop for what it holds, and painting it put a ring of folder colour around every level of the map. What shows instead is the map's own surface, which is the neutral grey that was there before and that I should not have taken away: it is not the window background, it is the map's, and it follows the theme in both directions.
Two things the map got wrong, both of them about a number that was not a number. **Blocks looked chewed.** Every visible seam was supposed to be the renderer's 1px, but the layout inset each directory's children by another 1px, so the two stacked: two siblings showed a 1px seam and two blocks from different parents showed 3px. Measured along one row of the real window: runs of 1, 2 and 3 pixels. `padding_px` is gone, so a child's frame is its parent's frame whole and every seam is the same 1px at any depth. Re-measured: every run is 1. **A fixed child limit cannot be right.** Two hundred children in one folder and two hundred in another are the same number and not the same picture — the first might be a wall of specks and the second a clean grid. What matters is how much room the folder has, so the limit is now a capacity derived from its own area: each child gets at least `COMFORT_PX` squared to sit in, or the folder folds. At 32px that is 100 children in a 320×320 block and 39 in a 200×200 one; above four, so the count alone never folds a directory holding a handful. The tests that pinned the padding are replaced by one that pins the absence of it — every frame in a chain is the frame the root was handed — and by one where the same directory folds in a small window and opens in a large one, which a fixed limit cannot do.
`#3b4a6b` was chosen as a colour without being looked at as a *surface*, and folders cover more of the map than anything else — a dark, low saturation blue over that much area reads as a muddy wash rather than as a palette. `#55618f` is the same family with the value and saturation lifted, which does two things at once: the map gets a tone instead of a murk, and a plate now sits clearly above the backdrop so a block reads as raised rather than as a slightly different patch of background.
The adaptive rules are what the map should do by default, but they had displaced the fixed-depth layout entirely, and a fixed depth is a legitimate thing to want from a treemap. Depth now offers both: **Auto** is the adaptive layout, and **All**, **1**, **2**, **3** are the original levels, where the cap is the whole rule. That is not just a shorter Auto. `adaptive_depth: false` stands the legibility gates down — a cap takes every level it is allowed to and asks nothing about whether the level is worth taking — which is what makes the four levels a real alternative rather than four ways to say the same thing. It comes with the picture that implies: All goes back to a mesh of small blocks. That is the original behaviour, deliberately, and it is no longer the default. Two things follow from the gates standing down. Opening a plate by hand is a request to override the legibility rules, so it only means something under Auto; elsewhere a click on a folder goes back to zooming, which is the same "show me inside this" answered the other way. And an open folder is dropped when the depth changes, rather than left set to something the map is not doing. The caps nest — cap 1 is cap 2 above its line — so the control is a dial and not four unrelated pictures, and switching cannot make the blocks above the line jump.
Opening a folder waited 300ms to find out whether the click was half of a double click. That wait was the whole of the "delay" — the expand itself was instant — and it made the double click worse than either gesture alone: nothing visible on the first click, then a zoom, so the two halves read as a pause and then an unrelated jump. Both go away by letting each click do its own thing. A click on a plate opens it immediately; the second click of a double click then zooms the folder that click opened — by identity, not by hit test, because by then the layout has been replaced by what that folder held and the cursor is over one of its children. And the opening is animated, since it is the one layout change that is demonstrably the same blocks at different sizes: the plate the user clicked becomes what was inside it. Every rect that existed before keeps its identity and slides to its new frame, and the ones that appear grow out of the plate's old rect — which needs no parent lookup, because everything revealed is inside that one rect by construction. Only this change is animated; a scan tick, a resize or a filter snaps, because those are not the map moving, they are the map being redrawn.
Growing the children out of the plate looked like what it was: three hundred blocks leaving the same point at once, each carrying the sheen that makes a block read as raised, stacked on top of each other until the middle of the plate went white. A flashbang, and the wrong shape for the change — opening a folder adds rects *inside* the one that was clicked and moves nothing else, so there is nothing to travel between and nothing should travel. It is a crossfade now: the picture as it stands fades into the one that replaces it. Same duration, same immediacy, no flare.
Author
|
Should I tweak the color of the dir? |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2.
Thirteen commits that make the treemap readable: blocks that tile instead of
leaving holes, folders drawn as blocks rather than as a texture, labels that
cannot move the geometry, and a Depth setting that offers the adaptive layout
and the fixed levels as real alternatives.
The floor raises small blocks instead of culling them
min_side_pxwas read as a cut-off line: a child under it was skipped, butits share of the frame stayed reserved, so a directory that was mostly small
files came out as a patch of flat nothing. It is a floor now — a child below
the line is raised to one minimum block and drawn anyway, so the directory
tiles edge to edge. What still does not fit at that size is dropped
smallest-first, and the survivors re-normalize into the space, so dropping
leaves no hole either. SpaceSniffer does the same, and it is the right way
round: nobody opens this to look at 4 KB files.
A row can still come out too thin to read — one file holding 99.985% of a
block squeezes the rest into a 0.09px column, which hit-testing then wins for
the whole height of it, because it walks deepest-first. Laying out into a
scratch row list before emitting anything lets the fit check the result and
shed from the first row that gives out.
Depth is decided by pixels, not by a count
A directory subdivides only while three things hold: its interior can hold a
legible child, its biggest child would land well clear of the floor (twice
it —
DETAIL_FACTOR), and it has room to give each child a block worthlooking at (
COMFORT_PX, 32px squared — a capacity derived from the folder'sown area, not a fixed count, because two hundred children in a narrow folder
and two hundred in a wide one are the same number and not the same picture).
Without the middle gate, folders nest into folders of specks: every level
spends padding to cut the next one finer, until the map is a field of 6px
blocks nobody can read or point at. Measured on
/usr/share(337k nodes):3686 rects, down from 6969 with the gate off.
Seams are one width
Every visible seam was supposed to be the renderer's 1px, but the layout
inset each directory's children by another 1px, so the two stacked: two
siblings showed a 1px seam and two blocks from different parents showed 3px.
Measured along one row of the real window: runs of 1, 2 and 3 pixels.
padding_pxis gone, so a child's frame is its parent's frame whole andevery seam is the same 1px at any depth.
Folders are drawn like files
A folder the layout stopped short of subdividing read as nothing there at
all, which made "show one folder" show an absence. Plates are a flat fill at
FOLDER_PLATE(#55618f) — a colour picked as a surface this time, sincefolders cover more of the map than anything else and a dark, low-saturation
blue over that much area reads as a muddy wash — with the same 1% gap and
the same sheen files have. The grain tile, its cache and the seam pass are
gone: a 1px gap against the backdrop does what the seam was invented to do.
A directory that did subdivide is no longer painted at all — it is only
the backdrop for what it holds, and painting it put a ring of folder colour
around every level of the map. What shows instead is the map's own surface,
the neutral grey that was there before.
Labels cannot move the geometry
Turning labels on used to reserve a strip at the top of every directory for
its name, so the geometry with labels was not the geometry without them.
label_pxis gone from the layout entirely andget_treemapno longer takesa
labelsflag — there is now no way for the geometry to know, so togglingis a repaint rather than a refetch. Names are painted into blocks that have
room for them, and only into solid ones: a block the layout subdivided is
covered by its children, and a name centred in it would land on top of them.
A click acts at once, and only the map moving is animated
Opening a folder waited 300ms to find out whether the click was half of a
double click. That wait was the whole of the delay — the expand itself was
instant — and it made the double click worse than either gesture alone:
nothing on the first click, then a zoom, so the two halves read as a pause
and then an unrelated jump. Both go away by letting each click do its own
thing. A click on a plate opens it in place immediately; the second click of
a double click then zooms the folder that click opened — by identity, not by
hit test, because by then the layout has been replaced by what that folder
held and the cursor is over one of its children.
The opening is animated, since it is the one layout change that is
demonstrably the same blocks at different sizes. An earlier attempt grew the
children out of the plate and looked like what it was: three hundred blocks
leaving the same point at once, each carrying the sheen that makes a block
read as raised, stacked until the middle of the plate went white. It is a
crossfade now. Every other change — a scan tick, a resize, a filter — snaps,
because those are not the map moving, they are the map being redrawn.
Depth offers both layouts
The adaptive rules are what the map should do by default, but they had
displaced the fixed-depth layout entirely, and a fixed depth is a legitimate
thing to want from a treemap. Depth now offers Auto (the adaptive layout)
beside All, 1, 2, 3 (the original levels, where the cap is
the whole rule).
That is not just a shorter Auto.
adaptive_depth: falsestands thelegibility gates down — a cap takes every level it is allowed to and asks
nothing about whether the level is worth taking — which is what makes the
four levels a real alternative rather than four ways to say the same thing.
It comes with the picture that implies: All goes back to a mesh of small
blocks, deliberately. The caps nest, so the control is a dial and not four
unrelated pictures, and switching cannot make the blocks above the line jump.
Smaller things in here
layout_with_forcetakes one id and opens it plus every ancestor betweenit and the layout root. One value carries the whole accordion: asking for a
directory outside the open one leaves the old one off the chain, so it
closes. A forced layout is a superset of the unforced one — pinned as
that rather than as the weaker prefix property — which is what lets the map
expand in place without a jump.
the only brake on a tree whose depth is unbounded, and a click that lands
deep should not be what removes it.
set to something the map is not doing.
was holding". A row is fit or unfit only as part of a whole layout — the
same run of equal blocks tiles legibly at some counts and not others — so
stopping at the first failure left a directory of equals as one plate.
it: their subject is the legibility rules, and their 210 and 640 children
were incidental to that.
Heads up, unrelated to this PR
cargo clippy --workspacefails oncrates/scanner-ntfs/src/record.rs:276with
clippy::chunks_exact_to_as_chunks, which is new in clippy 1.98(
-D warningsmakes it fatal). The repo doesn't pin a toolchain and CI uses@stable, so I think main is red for this too. Happy to send that fixseparately if useful.