Skip to content

All the new GUI features combined - #5702

Open
SimonDanisch wants to merge 78 commits into
ff/breaking-0.25from
sd/breaking-gui
Open

All the new GUI features combined#5702
SimonDanisch wants to merge 78 commits into
ff/breaking-0.25from
sd/breaking-gui

Conversation

@SimonDanisch

@SimonDanisch SimonDanisch commented Jul 16, 2026

Copy link
Copy Markdown
Member

This merges:
#5510
#3491
#5650
#5628
While it's not amazing to have them all in one big PR, i did want to heavily test and document all of them together, and make sure we really nail the event system and polish some other features we may need for more complex GUIs.

dashboard_demo.mp4

SimonDanisch and others added 30 commits December 19, 2023 12:39
Introduces `Makie.derive_colors(; accent, gray, background)`, a small
function that turns three user inputs into nine UI role colors
(background, surface, surface_subtle, border, text, text_muted,
text_on_accent, accent, accent_subtle), inspired by the Radix UI custom
palette tool. The default inputs reproduce Makie's previous Block
defaults within rounding so existing visuals don't shift.

`MAKIE_DEFAULT_THEME` now contains a nested `colors` Attributes block
populated from `derive_colors()` at module load, sitting next to
`fonts`. Block defaults can then read from it (see follow-up commit),
and users can recolour every interactive widget at once via
`set_theme!(colors = derive_colors(accent = :crimson))` or override
individual roles with `set_theme!(colors = (text = :navy,))`.
Every interactive Block now pulls its color defaults from the nested
`colors` theme block: Button, Checkbox, Toggle, Slider, IntervalSlider,
Menu, Textbox. State conventions are unified across blocks — idle uses
`surface`, hover uses `accent_subtle`, and active/checked/focused uses
`accent`.

Mechanically, this required:

- Extending `@inherit` (used inside `@Block` attribute declarations) to
  accept a tuple-of-symbols key path, e.g. `@inherit((:colors, :accent),
  fallback)`. The single-symbol form keeps working unchanged. The
  runtime walk is performed by a new `_inherit_nested` helper.
- Deleting the `COLOR_ACCENT` / `COLOR_ACCENT_DIMMED` `Ref`s: their last
  remaining uses (Block defaults and the Textbox cursor animation) now
  read from the theme instead. The cursor base color now follows the
  textbox's own `bordercolor_focused`, so a per-Textbox theme override
  travels through to the cursor too.
- Adding a `textcolor_active` attribute on Menu and rendering option
  text per-row, so the highlighted dropdown row gets `text_on_accent`
  (readable on the accent fill) while inactive rows keep base
  `textcolor`. Menu's previously-hardcoded `:black` `textcolor` and
  `dropdown_arrow_color` now route through `@inherit(:textcolor, ...)`
  so dark themes work without further overrides.
Two small menu follow-ups:

- The mouse handler short-circuits its end-of-frame cleanup with an
  early return when the cursor is inside the dropdown, so the menu
  button's `color_selector` was never reset back to its inactive
  swatch. Result: clicking the button to open the menu and then moving
  the cursor down into the dropdown left the button stuck in its
  hover-coloured state until the menu was closed. Now we reset the
  selector at the top of the dropdown branch, before the early return.
- Soften `dropdown_arrow_color` from full `textcolor` to the
  `text_muted` role so the arrow reads as a subtle ornament rather
  than a contrasty glyph, matching the original `(:black, 0.2)` intent
  but in a theme-aware way.
Extends `@inherit` to allow the nested-tuple form without a default
value: `@inherit((:colors, :surface))` instead of
`@inherit((:colors, :surface), RGBf(0.94, 0.94, 0.94))`. When the key
is missing from both the scene-local theme and the current global
theme, `_inherit_nested` now raises a clear error pointing at the
missing key path.

The `colors` block is always populated by `MAKIE_DEFAULT_THEME`, so the
literal fallbacks were dead code that obscured what each Block default
actually resolves to. If a user clears the block from their theme,
they will hit a fail-loud error pointing at the key they removed,
which is what we want.
Documents the nested `colors` theme block, the nine role tokens that
Block defaults read from, and the `Makie.derive_colors` helper that
turns an accent / gray / background trio into a full scheme. Includes
two worked examples — a warm cream/terracotta light theme and a dark
green theme — rendered with the same `widget_showcase` figure.
Lets the figure auto-fit its content (so the Menu row stops cutting
text) instead of pinning row sizes against a fixed Figure size.
`derive_colors` now interpolates `background` against `gray`/`accent`
in Oklab perceptual space instead of sRGB linear. Equal mix weights
produce equal perceived lightness steps, which keeps the neutrals well
balanced under tinted gray poles and non-default backgrounds (warm
light themes, off-black dark themes, etc.) rather than swinging toward
whichever pole has higher sRGB luminance.

This shifts the default light-mode neutrals away from the historical
hand-tuned literals:

- `surface` ≈ 0.92 (was 0.94)
- `surface_subtle` ≈ 0.96 (was 0.97)
- `border` ≈ 0.74 (was 0.80)
- `text_muted` ≈ 0.50 (unchanged; weight tuned from 0.50 → 0.40 to
  keep the muted-text luminance familiar)

Other roles (`background`, `text`, `accent`, `text_on_accent`) are
unchanged. The `accent_subtle` shift is well under a percent per
channel. Tests and changelog updated accordingly.
Move the Oklab mix helper into `utilities.jl` as `lerp_oklab` so it
sits next to the existing sRGB `lerp`, drop the private `_mix_oklab`,
replace the hand-rolled `gray === automatic` branch with
`default_automatic`, and use `Attributes(::NamedTuple)` directly
instead of splatting `pairs(...)`.
Collapse the two near-duplicate methods of `_inherit_nested` into a
single function with a `_NO_DEFAULT` sentinel, and replace the
imperative `cur::Any` + boolean-flag walk with a type-stable tuple
recursion through `_walk_nested(cur, keys_path)`. Also reject the
degenerate `@inherit(())` form in `make_attr_dict_expr` so it falls
through to the existing "must be a :symbol or tuple of symbols" error
instead of compiling.
…ipping

`forward_events!(target, source; active)` lets a subtree own its own `Events`
object and receive input only while `active`; window/render-context events
are always forwarded, so a hidden subtree still gets `tick` and resize updates.
`Consume` returned by `target` listeners propagates back, and held mouse
buttons / keys are released on deactivation to avoid stuck state.

`scene_visible(scene)` walks ancestor visibility so a `Block` that
force-shows its own scene still appears hidden under an invisible parent.
`effective_viewport(scene)` is the intersection of `scene` and its
ancestors viewports; `needs_ancestor_clip(scene)` returns `true` only
when at least one ancestor actually clips the scene.

CairoMakie now clips to `effective_viewport` (its previous behaviour was a
subset of this), and GLMakie enables `glScissor` to the effective viewport
only when an ancestor clips. Both backends skip rendering scenes that
fail `scene_visible`. The combined effect is that overflowing children
(e.g. scrolled axes) get cut at their container, while existing axis
markers near the spines still extend past the plot area as before.
…youts

Adds a `GridLayout` method that recurses into `gc.content` so blocks placed
inside another block`s layout — e.g. an `Axis` inside a `Tabs` tab — also
get their pre-display updates (auto axis limits, etc.). Previously only
top-level blocks in `fig.content` were visited, and nested ones missed
their auto-limit computation on the first display.
…tent

Adds a `Tabs` block that contains a row of clickable tab headers and one
content scene per tab. Place blocks via `tabs[i][row, col] = Axis(...)`
(like a mini-figure) or plot directly into `content_scene(tabs, i)`.

Each tab owns two scenes: a `clip` scene with viewport = visible content
area, isolated `Events`, and visibility tied to `active == i`; and an
`inner` scene with viewport = root figure viewport so that blockscenes
placed underneath it map decoration coords (which the layout reports in
absolute window pixels) 1:1 onto the window. Combined with the ancestor
visibility / scissor cascade, inactive tabs receive no mouse or keyboard
events and render nothing, while overflowing scrolled content clips
cleanly at the content area.

Content size derives from `GridLayoutBase.determinedirsize`, so setting
fixed col/row sizes on `tabs[i]` makes the tab scrollable; smaller
content fills the viewport. Mouse wheel / trackpad scroll over the tab
runs at a low priority, so an inner Axis zoom-on-scroll consumes first
and the container only scrolls when nothing else used the event.

Visible scrollbar tracks + thumbs appear when content overflows, with
hover and drag highlighting; the active thumb stays highlighted through
the drag even if the mouse leaves it. Headers are square white tabs
adjacent to each other (cmux style), with a single thin separator line
that detours up and around the active tab so it merges with the panel
below. Tabs work nested inside another Tabs tab.
Headless tests that drive root scene events manually:
- forward_events! gates input events by `active`, forwards context events
  unconditionally, propagates Consume back to the source, and releases
  held mouse buttons / keys on deactivation.
- Tabs: each tab content scene owns a distinct Events object; keyboard
  events only reach the active tab.
…h effective_clip

Collapses the two helpers into one: `effective_clip(scene)` is the
intersection of ancestor viewports *excluding* the scenes own. Backends
unconditionally enable scissor at that rectangle, which:

- still lets plots near a scenes edge (e.g. axis markers at the spines)
  extend past the scenes own viewport, since the scissor is the parents
  bounds rather than the scenes own;
- still clips scenes that overflow an ancestor (scrolled containers) at
  the ancestor edge;
- removes the conditional in GLMakies plot draw, so the scissor logic
  is the same shape on both backends.
The HoverMenu bar and its buttons used hardcoded dark-gray-on-white colors
that clashed with the themed widgets. Default its attributes to the `colors`
theme block via @inherit (surface bar with a border-colored stroke; buttons
that use surface/accent_subtle/accent + text/text_on_accent like every other
Button), and add button_color_active/label_color_active/bar_strokecolor plus a
:regular font so the toolbar matches the rest of the widgets and the Tabs.
…nts!)

Per the preferred design, isolate inactive tabs via the table PR's scene-stacking
event router (receives_events, honored by is_mouseinside/addmouseevents!) plus
scene visibility, rather than the tabs PR's forward_events! + isolated Events.

Verified: with a tab's Subfigure on shared events, scrolling over a *hidden*
tab's Axis does not change its limits (no leak), while the active tab's Axis
zooms — so visibility + receives_events already keeps hidden subtrees inert.
forward_events!/isolated Events are no longer used by Tabs.
Block geometry (computedbbox, slider endpoints) is in absolute window-pixel
coordinates and blockscenes use the window-absolute campixel! camera, but the
mouse-state machine hit-tested and reported positions with mouseposition_px,
which is viewport-relative. For widgets whose blockscene inherits an offset
viewport — anything placed inside a Subfigure or Tabs content scene — the two
frames never matched, so Toggle/Checkbox/Button/Slider were dead there.

Thread a `to_px` function through _addmouseevents!: the bbox variant (used by
the widgets) hit-tests and reports `event.px` in absolute window coords, while
the scene/elements variant (Axis, Textbox) keeps viewport-relative px so axis
interactions and data-space unprojection behave as before.

Verified: Toggle/Checkbox/Button click and Slider drag now work inside a Tab;
standalone widgets, axis rectangle-zoom and ctrl+click limit reset unchanged.
Modal: a theme-styled dialog replacing the ad-hoc KJgui Popup. A translucent
backdrop dims the figure and blocks pointer input to everything underneath
while open; the centered body auto-sizes to its content (or scrolls at a fixed
width/height — the content area is a Subfigure parented under the overlay so
it shares the pointer-cover's subtree). Header with themed title, separator
and a Tabs-style close ×; open!/close!/isopen API; optional dismissal by
backdrop click; place content via modal[row, col].

Pointer capture: covers_pointer() previously required clear=true + z>0, which
a translucent overlay can't satisfy (glClear can't alpha-blend a backdrop).
Scenes now opt in explicitly with `scene.captures_mouse = true` — the modal
overlay uses this, making it fully modal.

Verified end-to-end: widgets inside the modal (Toggle/Slider/Button) work;
a Table underneath receives no clicks while the modal is open; backdrop click
dismisses; the table is interactive again afterwards.
@github-project-automation github-project-automation Bot moved this to Work in progress in PR review Jul 16, 2026
@hz-xiaxz

Copy link
Copy Markdown
Contributor

This is so damn cool!

# text-input UX where the highlight disappears on click-away.
visible = plot.focused,
space = :pixel, transformation = :nothing, inspectable = false,
space = plot.space, transformation = :nothing, inspectable = false,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work with any space other than pixel?

# end
scene.visible[] || return false
in(Vec(scene.events.mouseposition[]), viewport(scene)[]) || return false
return receives_events(scene)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would a scene need to receive events for the mouse to be inside it? If there are multiple scenes where only one has focus it seems pretty natural to me to switch between them by clicking on an inactive one with a if is_mouseinside(inactive_scene) check

Comment thread Makie/src/camera/camera2d.jl Outdated
Comment on lines +332 to +338
# Project absolute window pixels (not viewport-local) to NDC so a coord
# `(X, Y)` in a campixel scene renders at window pixel `(X, Y)` regardless
# of where the scene sits in its parent. For a scene whose viewport is at
# the window origin this is identical to the old `(0..w, 0..h)` projection.
vx, vy = Float64.(minimum(window_size))
w, h = Float64.(widths(window_size))
projection = orthographicprojection(0.0, w, 0.0, h, cam.near, cam.far)
projection = orthographicprojection(vx, vx + w, vy, vy + h, cam.near, cam.far)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So with this you wouldn't be plotting to a (0..w, 0..h) range which is fully defined by the local scene, but a (x..x+w, y..y+h) range which is only defined in the context of the whole scene tree? That does not seem like a good change to me. And unnecessarily breaking. I'd rather see a new camera for this if it's needed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was the other way around, campixel seemed to plot to x,y in window coordinates regardless of scene position, and I thought it should be relative to the scene viewport instead. So as far as I remember I changed campixel to work that way and a fallback for the old behavior was added

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this pr, on breaking:

using GLMakie
scene = Scene(camera = campixel!)
left = Scene(scene, viewport = Rect2f(0, 0, 100, 100), camera = campixel!, clear = true, backgroundcolor = :lightblue)
scatter!(left, Rect2f(5, 5, 90, 90), color = :blue)
right = Scene(scene, viewport = Rect2f(300, 300, 100, 100), camera = campixel!, clear = true, backgroundcolor = RGBf(1, 0.8, 0.8))
scatter!(right, Rect2f(5, 5, 90, 90), color = :red)
scene
image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe I'm misremembering, it was a while ago..

Comment on lines +20 to +28
g.box = Box(
g.layout[1, 1:3];
height = g.height,
width = g.width,
color = g.bar_color,
cornerradius = g.corner_radius,
strokewidth = 1,
strokecolor = g.bar_strokecolor
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With complex/block recipes this can just be Box(g[1, 1:3], ...). @forwarded_layout is gone now, iirc.

Note that with nested attributes we can now have g.box.color etc. as attributes. That would take priority over g.box as a field in getproperty, iirc, but that shouldn't be a problem if all the relevant attributes are part of the HoverMenu. You can also do

@attributes begin
    box = @attributes begin
        documented_attributes(Box)...
    end
end

within a @Block. filtered_attributes should work too. You can still get the blocks from g.blocks or the internal layout outside initialize_block!() if you need them.

Comment thread Makie/src/interaction/events.jl Outdated
Comment on lines +119 to +128
for field in CONTEXT_EVENT_FIELDS
s = getfield(src, field)
d = getfield(dst, field)
push!(
obsfuncs, on(s; priority = priority) do value
d[] = value
return Consume(false)
end
)
end

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that all events of a child/target happen at the same priority in the parent/source. With default priority=0 you'd have an order like this:

30 parent interaction 1
1 parent interaction 2
0.20 child interaction 1
0.-200 child interaction 2
-1 parent interaction 3

Adding a parent interaction with priority 0 could happen before, after or between any of the child interactions.

That's what you naturally get by just forwarding events to another Observable and it never seemed right to me. I always though that priorities should be preserved, so you'd get this order:

30 parent interaction 1
  20 child interaction 1
1 parent interaction 2
-1 parent interaction 3
  -200 child interaction 2

That's what we currently have by just using the identical events object in every scene. I guess if we want to keep that we'd need a new Observable type that manages a shared pool of listeners alongside a local one?

Comment thread Makie/src/makielayout/types.jl Outdated
Comment on lines +1530 to +1535
"The horizontal alignment of the subfigure in its suggested bounding box."
halign = :center
"The vertical alignment of the subfigure in its suggested bounding box."
valign = :center
"The alignment of the subfigure in its suggested bounding box."
alignmode = Inside()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are added automatically (with the same values) by mixin_block_layout_observables. Could remove them.

Comment thread Makie/src/makielayout/types.jl Outdated
Comment on lines +1677 to +1690
"The height setting of the table."
height = Auto()
"The width setting of the table."
width = nothing
"Controls if the parent layout can adjust to this element's width."
tellwidth = true
"Controls if the parent layout can adjust to this element's height."
tellheight = true
"The horizontal alignment of the table in its suggested bounding box."
halign = :center
"The vertical alignment of the table in its suggested bounding box."
valign = :center
"The alignment mode of the table in its parent GridLayout."
alignmode = Inside()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only width is needed here

Comment thread Makie/src/makielayout/types.jl Outdated
A clipped, optionally event-isolated, scrollable region with its own
`Scene` paired with a `GridLayout` — the same shape as a `Figure`, scoped
to a sub-region. Place blocks via `Axis(subfig[1, 1])` etc., or plot
directly into `content_scene(subfig)` (in viewport-local pixel coords).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subfigure makes me think this is just an area of a figure that acts as figure itself, like the stupidly simple Container block I added. But this also has a bunch of plots and the scroll interactions too... Makes me wonder if this is more than it should be, or if it should be split up into multiple things. I.e. should the plots be something that is added to a Subfigure (e.g. via Label, Box, ...) rather than being part of it directly? And should the scrolling be an add-on? Or perhaps the difference between Container and Subfigure? (which should be named more appropriately then)

Comment thread Makie/src/bezier.jl Outdated
Comment on lines +119 to +124
# NB: don't use `union(bb, _bb)` here. GeometryBasics considers any
# Rect with a zero-width dimension "empty" and returns the other
# operand, so accumulating the bboxes of axis-aligned segments
# (whose segment bbox is legitimately zero-width/height, e.g. a
# rectangle marker) would drop earlier segments and yield a
# degenerate path bbox. Grow the corners directly instead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also fixed by JuliaGeometry/GeometryBasics.jl#276 / GeometryBasics 0.5.11

Comment on lines +1 to +18
"""
covers_pointer(scene::Scene) -> Bool

True when `scene` is the topmost layer over its viewport: visible, with
`clear[] == true` (paints its own background) AND a positive world-z. A
scene with only one signal — a Legend lifted to z=10, or an Axis whose
theme sets `clear=true` at z=0 — is layered for rendering only and does
not claim pointer input.

Scenes that paint a translucent backdrop via plots instead of an opaque
`clear = true` background (e.g. a modal dialog's overlay) can opt in
explicitly with `scene.captures_mouse = true`.
"""
function covers_pointer(scene::Scene)
scene.visible[] || return false
scene.captures_mouse && return true
return scene.clear[] && z_world(scene) > 0
end

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this function. The mouse pointer/cursor is always on top of what's drawn, isn't it?

True when scene is the topmost layer over its viewport

It doesn't check that?

visible

Whether a scene is visible or not doesn't really matter for event processing? We could forbid it, but I don't see why we would

clear[] == true

If clear worked as expected you could say that you can't interact with a parent scene if it's covered by a child scene that clears everything the parent scene draws. But that's not how clear works atm, at least not in all backends. See #4150, #4724. It also only makes sense when you're talking about interacting with visual elements, which may not always be the case.

positive world-z

This scene having positive z does not guarantee that it covers things from it's parent. The parent can still draw at higher z, or plots in this scene could leave gaps. Negative z in this scene may also still draw over its parent. And in 3D this doesn't work, because z != depth.

Also what about sibling scenes? They could cover this scene too, if that's what this function or more generally the functions in this file are trying to figure out. Note though that the render order of scenes can be inconsistent between backends so any what-covers-what analysis is risky atm. (Again, see #4150, #4724)

interaction_record's Wait is fixed-duration, which can't handle async work
whose time varies with recording load (e.g. a GPU analysis). WaitUntil
records frames until pred() holds (or timeout), so a demo never advances
past a still-running operation onto a half-finished result.
A scene whose viewport hasn't been solved yet carries the empty-Rect2i() sentinel
(typemax/typemin origin+widths). effective_clip's Int64 intersect then overflows to
a garbage-but-finite rect, and round(Int, ppu .* that) for glViewport/glScissor
threw InexactError('Error while rendering!') on the very first frame of a fresh
window. Clamp each pixel extent to a sane GLint range via gl_extent so a transient
pre-layout viewport degrades to a valid rect instead of killing the render loop.
- ParamForm takes a positional `accessory` builder `(field, gridpos) ->
  block` to add a third widget per field row (e.g. a keyframe toggle);
  returned blocks are stored in pf.accessories, column width via
  accessorywidth.
- Modal now honors its halign/valign attributes when placing the body
  instead of always centering, so a dialog can be docked to a side and
  keep important content (e.g. a video preview) visible.
…dren

- text: clamp per_glyph attribute blocks to the current glyph count —
  growing a Menu's options while open indexed past the stale blocks
- subfigure: hide!/show! walk the child blocks, so a hidden dock panel
  no longer keeps invisible-but-clickable widgets that steal clicks
- pin the mono-repo compat versions for the dev setup
@chriswaudby

Copy link
Copy Markdown

Super exciting!

A NESTED subfigure (e.g. a scrollable panel inside a dock-slot subfigure)
went permanently blank after hiding and re-showing the parent: the
parent's child-visibility walk applies the generic hide!, which
force-sets the nested subfigure's content scene invisible — overriding
its reactive binding to `sf.visible`. The specialized unhide! then only
restored the blockscene and deliberately left the scene to the binding,
but nothing ever re-fires that binding (the NESTED subfigure's own
`visible` never changed), so the scene stayed invisible and every child
unhide! early-returned on the invisible parent.

unhide!(::Subfigure) now re-syncs `sf.scene.visible` with `sf.visible`
instead of trusting the stale binding: a nested subfigure whose own
`visible` is false stays hidden, one that should show gets its scene
back BEFORE the parent-first walk reaches its children.
A block's mouse machinery gates on its own `blockscene.visible` and its own
bbox — and a scrollable subfigure moves content past its viewport freely, so a
widget scrolled out of sight still sat somewhere in the window and swallowed
presses meant for whatever is drawn there. Measured in an editor panel: a 1213 px
tall dock in a 714 px viewport put five buttons BELOW it, straight over the
timeline (a "Loop finder" header at y 116..149, x 90..358), where they ate every
click — the timeline never saw the press, and the failure surfaced three
interactions later as "the crop went to the wrong clip" and "space stopped
toggling playback".

Content that does not intersect the viewport is now hidden, the same rule the
subfigure already applies when it is hidden as a whole. Partially visible blocks
stay live.
A query with no matches emptied the per-option color vectors. The next
query that DID match pushed the new strings into the option text plot,
which resolves eagerly (the list height listens to its glyph bounding
boxes) — before anything had resized the colors. per_glyph_block then
indexed data[0] of an empty vector and threw inside the compute graph,
which takes the whole GLMakie render loop with it: the window stops
responding to anything from then on.

Positions, rects and colors are now resized in a handler on optionstrings
with priority 1, i.e. ahead of the text plot's own listener, so the order
is enforced rather than incidental. per_glyph_block additionally refuses
to index an empty attribute vector — an error in there is never worth a
dead render loop.
@ffreyer

ffreyer commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Maybe closes #208

Comment thread Makie/src/makielayout/blocks/spinner.jl Outdated
my_gen = gen[]
@async while sp.running[] && gen[] == my_gen
sp.visible[] && (frame_idx[] = frame_idx[] + 1)
sleep(sp.frame_interval[])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this kind of stuff is generally much nicer with ticks now, instead of spinning up async tasks. it's also better for generating videos at non-real-time

Rebuilding a Modal's content in place left the rows and columns that
deleting blocks leaves behind, so a modal that had held a longer list
stayed sized for it. replace_content! deletes the content, trims those
tracks, runs the callback and resyncs the content size.

max_size caps the auto-sized body so longer content scrolls instead of
growing the dialog off the figure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Work in progress

Development

Successfully merging this pull request may close these issues.

5 participants