[WIP] Use pygame for simpler rendering#147
Draft
sastraxi wants to merge 58 commits into
Draft
Conversation
This reverts commit 44876e9.
# Conflicts: # pistomp/lcd320x240.py # tests/snapshots/test_lcd320x240/test_analog_assignments_snapshot/0.png # tests/snapshots/test_lcd320x240/test_main_panel_snapshot/0.png # tests/snapshots/test_lcd320x240/test_parameter_dialog_snapshot/0.png # tests/snapshots/test_lcd320x240/test_system_menu_snapshot/0.png # tests/snapshots/test_lcd320x240/test_tap_tempo_snapshot/0.png # tests/snapshots/test_lcd320x240/test_update_footswitch_off_snapshot/0.png # tests/snapshots/test_lcd320x240/test_update_footswitch_on_snapshot/0.png # tests/snapshots/v3/test_pedalboards/test_v3_pedalboard_change_via_lcd/0.png # tests/snapshots/v3/test_pedalboards/test_v3_pedalboard_change_via_modui/0.png # tests/snapshots/v3/test_plugins/test_v3_parameter_edit/param_closed.png # tests/snapshots/v3/test_plugins/test_v3_parameter_edit/param_dialog.png # tests/snapshots/v3/test_plugins/test_v3_parameter_edit/param_menu.png # tests/snapshots/v3/test_plugins/test_v3_parameter_edit/param_tweaked.png # tests/snapshots/v3/test_plugins/test_v3_parameter_midi_change/0.png # tests/snapshots/v3/test_plugins/test_v3_preset_change_plugin_update/0.png # tests/snapshots/v3/test_plugins/test_v3_toggle_plugin_bypass_direct/0.png # tests/snapshots/v3/test_presets/test_v3_preset_change_via_footswitch_longpress/0.png # tests/snapshots/v3/test_presets/test_v3_preset_change_via_lcd/nav_A.png # tests/snapshots/v3/test_presets/test_v3_preset_change_via_lcd/nav_B.png # tests/snapshots/v3/test_presets/test_v3_preset_change_via_lcd/nav_C.png # tests/snapshots/v3/test_presets/test_v3_preset_change_via_lcd/nav_D.png # tests/snapshots/v3/test_startup/test_v3_footswitch_press/0.png # tests/snapshots/v3/test_startup/test_v3_nav_to_system_menu/0.png # tests/snapshots/v3/test_startup/test_v3_startup_snapshot/0.png # uilib/container.py # uilib/image.py # uilib/panel.py # uilib/text.py
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.
pygame gives us "free" C-accelerated clipping rectangles (well, other than text rendering) that make it easier to write UI widgets that compose naturally. Here's this branch's
uilib/README.mdreproduced:Paint system
The UI is a tree of widgets, each widget knowing its own rectangle in its parent's coordinate space. Non-leaf nodes (
ContainerWidgets) each own apygame.Surfacethat holds the composite of itself and its descendants. Leaf widgets have no buffer of their own: they draw straight into the nearest ancestor surface. The root of the tree is aPanelStack, whose surface is the one pushed to the LCD.Drawing happens via
do_draw; aPaintContextis passed to each widget's concrete_drawmethod. This context includescliprect in surface coords, andframe.The context manager
PaintContext.painting(frame)builds a "sub-context" for drawing children. It uses SDL's capability to drop any primitive that strays outside the clip, so drawing methods can treat their own rect as if it were the whole world.Virtual painting
A container's surface is usually the same size as its box, but a virtual container can hold a surface taller (or wider) than its viewport. This is currently used for scrollable menus where content might run past the screen extents (we're working with a 320x240 LCD).
The container's
offsetfield is the (x, y) of the viewport's top-left within that "tall" surface, while_viewport_view()returns apygame.Surfacesubsurface of the cache at the current offset. For non-virtual containersviewport == bounds, so the view is the whole surface; for virtual containers it's a moving window. Either way, the same blit path serves both.Virtual containers do, however, diverge from the standard path in a couple ways:
Their
refresh()paints into "content" coordinates rather than "physical" ones, though children don't need to care about this because they draw in local coordinates anyhow.do_draw()skips the lazy-rebuild path that non-virtual containers use because their cache is maintained externally byrefresh()andscroll(): off-viewport children get a_dirtyflag so that scrolling lazily paints them as they come into view, without losing previously-painted pixels.Caching
Each container caches its composite, keeping track of which regions are pending re-draws via
_dirty_Region: Box | None.Nonemeans clean — the surface can be blitted as-is. A Box means that rectangle is stale and the rest of the cache is up-to-date.When
do_drawis called on a non-virtual container with a dirty region, it rebuilds only that slice: thepainting(frame)clip drops everything outside it, and children whose boxes don't intersect the rect are skipped entirely.Cache invalidation happens two ways:
The first method
propagate_dirty(clip)is called after pixels have been written somewhere (e.g. a leaf calledWidget.refresh(box)). New pixels exist, but every cached composite is stale (for a certain rectangle) up to the tree root. The new "dirty rectangle" is unioned (after coordinate translation) with ancestors' existing_dirty_rects.The chain terminates at
PanelStack.propagate_dirty, which is the onlypropagate_dirtythat actually does something visible: it composes the stacked panels into the root surface and pushes the result to the LCD.The second method
_invalidate_cache(box)is called when a widget is attached or detached from the widget tree; it uses the same logic to mark that area as stale.Masking
RoundedPanelintroduces a per-corner alpha mask. For non-virtual panels the mask is multiplied into the cache once, in the_finalize_cache()hook called at the end of every rebuild, so the panel blits out as plain pixels from then on. Virtual panels can't pre-multiply, because the mask applies to different parts of the backing surface (via the viewport): instead, they apply the mask per-blit against a temporary copy of the viewport slice.Subclasses define their shape by overriding
_build_shape_mask().