Perf fixes + Scrollbar/KeyValueView classes#43
Conversation
- Builds Scrolling (via scrollbar) into StackView as an opt-in path via css - Support for Image-buttons
…clamp
WindowController: replace the per-motion whole-tree hover scan with a
hit-test chain-diff (O(views*depth^2) -> O(depth)); mark only the
entered/left views for re-theme, not their subtrees (no ancestor:hover
cascade exists), killing the spike on crossing into a large container.
View: rewrite clippingFrame from a per-ancestor recursion (O(depth^2),
called per-view per-frame) to a single O(depth) top-down pass; result is
value-identical by construction.
Style: strip /* */ comments before tokenizing. The parser splits on
{ : ; } with no notion of comments, so a comment absorbed into a
selector silently broke the rule it preceded.
Select: keep an expanded menu on-screen by clamping it up from the
window bottom edge, in layoutSubviews (the earliest point its true
height is known).
Control: revert stateDidChange to subtree invalidateStyle -- state
pseudos (Checkbox:selected ImageView, etc.) do style descendants; the
self-only variant broke checkmark toggling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ability to disable tabs
Promoted from quetoo's cgame editor, where they were originally built as project-local classes. Scrollbar now binds directly to a StackView's own opt-in scrolling (contentOffset/scrollToOffset) instead of requiring a ScrollView wrapper. All four are generic, renderer-agnostic View subclasses with no cgame dependency.
There was a problem hiding this comment.
Pull request overview
This PR improves ObjectivelyMVC UI performance (hover tracking, clipping computation, and page layout), fixes CSS parsing around block comments, and introduces new UI components for scrolling and key/value layouts.
Changes:
- Reworked hover enter/leave dispatch in
WindowControllerand optimizedView::clippingFrameto avoid repeated ancestor recursion. - Added opt-in
StackViewscrolling (content offset + wheel handling) plus newScrollbar/ScrollThumbwidgets to drive it. - Added
KeyValueView/KeyValueTableViewand a disabled-tab state forTabViewItem; improved CSS parsing by stripping/* ... */comments.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| Sources/ObjectivelyMVC/WindowController.c | Replaces full-tree hover scanning with hit-test path diffing and marks views for re-theming. |
| Sources/ObjectivelyMVC/View.c | Optimizes clippingFrame from recursive to single-pass chain computation. |
| Sources/ObjectivelyMVC/TabViewItem.h | Adds TabStateDisabled enum flag. |
| Sources/ObjectivelyMVC/TabViewItem.c | Applies/removes "disabled" class based on tab state. |
| Sources/ObjectivelyMVC/TabView.c | Ignores clicks on disabled tabs. |
| Sources/ObjectivelyMVC/Style.c | Strips /* ... */ comments before tokenizing CSS. |
| Sources/ObjectivelyMVC/StackView.h | Adds opt-in scrolling fields and scrollToOffset API. |
| Sources/ObjectivelyMVC/StackView.c | Implements scroll styling, wheel scrolling, and offset clamping. |
| Sources/ObjectivelyMVC/SelectorSequence.c | Fixes isspace UB by casting to unsigned char. |
| Sources/ObjectivelyMVC/Select.c | Clamps expanded menu viewport to window bounds. |
| Sources/ObjectivelyMVC/ScrollThumb.h | Introduces draggable thumb control + delegate protocol. |
| Sources/ObjectivelyMVC/ScrollThumb.c | Implements thumb drag event capture and delegate callbacks. |
| Sources/ObjectivelyMVC/Scrollbar.h | Introduces vertical scrollbar API bound to StackView scrolling. |
| Sources/ObjectivelyMVC/Scrollbar.c | Implements scrollbar layout, styling, stepping, and thumb syncing. |
| Sources/ObjectivelyMVC/PageView.c | Avoids laying out hidden pages to reduce tab-panel layout stalls. |
| Sources/ObjectivelyMVC/Makefile.am | Adds new source/header files to the build. |
| Sources/ObjectivelyMVC/KeyValueView.h | Adds key/value row view API (two-column row). |
| Sources/ObjectivelyMVC/KeyValueView.c | Implements key/value row creation, inlets, and column clamping. |
| Sources/ObjectivelyMVC/KeyValueTableView.h | Adds vertical table API for shared column widths across rows. |
| Sources/ObjectivelyMVC/KeyValueTableView.c | Implements table-wide column propagation and row management. |
| Sources/ObjectivelyMVC/Control.c | Documents why control state changes must invalidate subtree styles. |
| Sources/ObjectivelyMVC/Button.c | Adds JSON inlet binding for button image. |
| ObjectivelyMVC.vs15/ObjectivelyMVC.vcxproj.filters | Registers new files in the VS filters list. |
| ObjectivelyMVC.vs15/ObjectivelyMVC.vcxproj | Registers new files in the VS project. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (size_t r = 0; r < len; ) { | ||
| if (css[r] == '/' && css[r + 1] == '*') { | ||
| r += 2; | ||
| while (r < len && !(css[r] == '*' && css[r + 1] == '/')) { | ||
| r++; | ||
| } | ||
| r = (r < len) ? r + 2 : len; | ||
| out[w++] = ' '; | ||
| } else { | ||
| out[w++] = css[r++]; | ||
| } | ||
| } |
| // Opt-in scrolling: when enabled, shift the stack by the (negative) scroll | ||
| // offset and clip the overflow. Disabled StackViews lay out exactly as before. | ||
| if (this->scrolls) { | ||
| pos = (this->axis == StackViewAxisVertical) ? this->contentOffset.y : this->contentOffset.x; | ||
| self->clipsSubviews = true; | ||
| } |
| */ | ||
| static void applyStyle(View *self, const Style *style) { | ||
|
|
||
| super(View, self, applyStyle, style); |
| */ | ||
| static void applyStyle(View *self, const Style *style) { | ||
|
|
||
| super(View, self, applyStyle, style); |
| } | ||
| } | ||
|
|
||
| super(View, self, layoutSubviews); |
| if (!isInChain(v, to)) { | ||
| $(v, emitViewEvent, ViewEventMouseLeave, NULL); | ||
| v->needsApplyTheme = true; | ||
| } | ||
| } |
| MakeInlet("-adorner-size", InletTypeInteger, &this->adornerSize, NULL), | ||
| MakeInlet("-adorner-shade", InletTypeInteger, &this->adornerShade, NULL), | ||
| MakeInlet("-orientation", InletTypeEnum, &this->orientation, (ident) ScrollbarOrientationNames), | ||
| MakeInlet("-bgcolor", InletTypeColor, &this->bgColor, NULL), | ||
| MakeInlet("-fgcolor", InletTypeColor, &this->fgColor, NULL), |
jdolan
left a comment
There was a problem hiding this comment.
ObjectivelyMVC already has ScrollView, that is specifically meant for wrapping anything you want to scroll, within a sized "viewport" (frame). It's what TableView uses internally, for example.
We should definitely not add ScrollView / ScrollBar functionality directly to StackView, whose primary responsibility is layout of its subviews. Maintain single-responsibility principle / separation of concerns.
Really, the ScrollBar stuff should all be added as an option (opt-in) on ScrollView.
Then, if you want to scroll over the contents of a StackView, you put that StackView inside a ScrollView. This is exactly what TableView does.
I would suggest we break this into several PRs:
- TabView disablement
- Select layout fixes
- View / WindowController perf fixes
- ScrollBar as an nullable member on ScrollView
The latter requires significant rework from the rest of this PR. Is this something that you want to run with, or do you want to hand it off? It's gonna be a little while if I take it..
| * is provided by an ancestor StackView with `scroll: true`. | ||
| */ | ||
|
|
||
| typedef struct KeyValueTableView KeyValueTableView; |
There was a problem hiding this comment.
Hm. This whole class feels like a workaround for either a CSS bug or a missing feature in our CSS engine? It should be possible to write a CSS rule that would apply to a given column for all rows -- which is basically all you're addressing here, right? The ability to say "key" is always 120px and "value" is always 240px or whatever, right?
One thing that would help is if TableView added CSS classes on each TableCellView based on its column headers. So in every row, the "key" cell would have classNames containing "key" for example.
Would that address this and make this class superfluous?
| * tabbed-panel layout stall. A page is laid out lazily when it becomes current: | ||
| * setCurrentPage unhides it and flags this view for layout. | ||
| */ | ||
| static void layoutIfNeeded(View *self) { |
| View *subview = subviews->elements[i]; | ||
|
|
||
| if (!subview->hidden) { | ||
| $(subview, layoutIfNeeded); |
There was a problem hiding this comment.
I mean, should this actually just be the behavior in View::layoutSubviews? Seems like it...
|
|
||
| const EnumName ScrollbarOrientationNames[] = MakeEnumNames( | ||
| MakeEnumAlias(ScrollbarOrientationRight, right), | ||
| MakeEnumAlias(ScrollbarOrientationLeft, left) |
There was a problem hiding this comment.
Shouldn't we also support top and bottom, for horizontal scrolling?
| /** | ||
| * @brief ScrollThumbDelegate; maps thumb travel onto the StackView's content offset. | ||
| */ | ||
| static void thumbDidDrag(ScrollThumb *thumb, int delta) { |
There was a problem hiding this comment.
You're gonna want a float for delta here. SDL3 uses floating point for mouse movement and scroll increments, because on high-DPI displays, sub-pixel (logical pixel) movement is definitely a thing.
|
|
||
| #include "ScrollThumb.h" | ||
|
|
||
| #define _Class _ScrollThumb |
There was a problem hiding this comment.
Also, Panel / Panel.contentView should probably get one of these huh?
| // Cast to unsigned char: isspace() is UB for negative values (non-ASCII bytes | ||
| // sign-extend to negative char), which the MSVC debug CRT asserts on. This | ||
| // also lets the parser tolerate arbitrary input bytes without crashing. | ||
| while (isspace((unsigned char) *c)) { |
| MakeInlet("distribution", InletTypeEnum, &this->distribution, (ident) StackViewDistributionNames), | ||
| MakeInlet("spacing", InletTypeInteger, &this->spacing, NULL) | ||
| MakeInlet("spacing", InletTypeInteger, &this->spacing, NULL), | ||
| MakeInlet("scroll", InletTypeBool, &this->scrolls, NULL) |
There was a problem hiding this comment.
Oof.
ObjectivelyMVC already has ScrollView, that is specifically meant for wrapping anything you want to scroll, within a sized "viewport" (frame). It's what TableView uses internally, for example.
We should definitely not add this functionality directly to StackView, whose primary responsibility is layout of its subviews. Maintain single-responsibility principle / separation of concerns.
Really, the ScrollBar stuff should all be added as an option (opt-in) on ScrollView.
Then, if you want to scroll over the contents of a StackView, you put that StackView inside a ScrollView. This is exactly what TableView does.
| * Each comment is replaced by a single space so it still separates adjacent tokens. | ||
| * The caller must free the returned buffer. | ||
| */ | ||
| static char *stripComments(const char *css) { |
| TabStateDefault = 0x0, | ||
| TabStateSelected = 0x1 | ||
| TabStateSelected = 0x1, | ||
| TabStateDisabled = 0x2 |
Summary
WindowController/View(was O(views * depth^2) on mouse motion, and O(depth^2) per clippingFrame computation) and self-onlyControlstate invalidation, instead of re-styling whole subtrees on hover changes./* */comments (a comment before a rule was previously getting absorbed into its selector, silently killing the rule).Selectviewport clamp fix.TabViewItemgains aTabStateDisabledstate (greys out and blocks clicks on a tab without removing it from theTabView).Scrollbar/ScrollThumbclasses: a visible, draggable vertical scrollbar bound directly to aStackView's own opt-in scrolling (scroll: true,contentOffset/scrollToOffset) — noScrollViewwrapper needed.KeyValueView/KeyValueTableViewclasses: a two-column key→value row (any View in either column) and a vertical table of such rows with shared, CSS-driven column widths (-key-width/-value-width).All of this was built and used in-tree for Quetoo's in-game material/entity editor, then generalized and promoted here since none of it is Quetoo-specific.
Test plan
main, including the SDL_gpu/ObjectivelyGPU renderer.Scrollbarbound to aStackView,KeyValueTableViewrows in the entity/material/mesh tabs, disabled mesh tab viaTabStateDisabled.🤖 Generated with Claude Code