Improve Pencuil immediate-mode authoring ergonomics #418

Description

@botoddly

Context

Pencuil's immediate-mode locality is useful, but its current API leaves common control, layout, identity, presentation, and invalidation work to each application.

The gameplay settings editor in Babka and the Pixely Hotbar tutorial expose the recurring friction:

  • Pencil.Button() registers interaction without drawing the button or advancing layout, so applications build buttons from Panel(), text measurement, and manual centering.
  • Panel() always registers hover and click tests, even when used only as a background or decorative rectangle.
  • Layout frequently requires absolute MoveTo() calls, manual text measurement, coordinate arithmetic, and saving/restoring Pencil cursor state around composed content.
  • Text and number fields accept raw integer IDs from a namespace shared by all views in a Pencuil root.
  • Every IPencuilViewModel exposes mutable IsDirty, and every state mutation must remember to set it.

Improve Pencuil itself while preserving immediate-mode authoring and rebuild-only-when-needed rendering.

Proposed improvements

Complete standard controls

Make Button() a complete control: draw its background, border, and text; expose its interaction state; support enabled state and fixed or content-derived sizing; and contribute its final bounds to the active layout.

Separate visuals from interaction

Use distinct operations for distinct intent:

  • Rectangle() or Fill() for non-interactive visuals;
  • Button() for ordinary button behavior;
  • HitArea() or an equivalent primitive for custom interactive regions such as hotbar slots.

Drawing a background must not silently register a full-window interactive area.

Add composable immediate-mode layout

Add layout scopes such as Row, Column, Padding, Align, and Overlay. A nested scope must correctly contribute its resulting bounds to its parent, so composing content does not require applications to preserve and restore CurrentPosition and CurrentSize.

The API should cover common fixed-size, content-size, stretch, gap, and alignment cases without introducing a retained element tree.

Scope control identity

Control IDs should:

  • be scoped automatically by the current view;
  • accept semantic keys such as enums without casts;
  • support nested scopes for repeated controls;
  • prevent collisions between independently composed views.

Add presentation conveniences

Directly support common operations that currently require measurement and coordinate plumbing:

  • horizontal and vertical text alignment;
  • wrapped text constrained to a maximum width;
  • centering inside the current layout region;
  • clipping and scrolling;
  • enabled, hovered, pressed, and selected visual states.

Applications should rarely call MeasureText() merely to position content.

Encapsulate dirty tracking

Keep conditional rebuilding, but remove IsDirty bookkeeping from application state. Provide a value-based wrapper along these lines:

publicreadonlyrecordstructGameplaySettingsViewState(floatCharacterSpeed,floatSowingDuration);ViewModel<GameplaySettingsViewState>viewModel=new(initialState);viewModel.Value=viewModel.Valuewith{CharacterSpeed=editedSpeed};

ViewModel<TValue> should require TValue : struct. Assigning Value should invalidate observers, backed by a monotonically increasing version rather than a consumable boolean. Each view tracks its own observed version, allowing multiple views to observe the same model safely.

Keep Value by value initially for assignment ergonomics. A ref readonly accessor can be considered later only if profiling demonstrates meaningful copies.

Actions and injected services remain separate from the state value; this is invalidation support, not mandatory MVVM or data binding.

Acceptance criteria

  • The Babka gameplay settings editor no longer needs its custom button implementation, integer ID casts, manual text centering, or most absolute coordinate arithmetic.
  • The Hotbar tutorial can compose an image inside each slot without saving and restoring Pencil cursor state.
  • Decorative backgrounds do not participate in hit testing.
  • Independently authored views and repeated control groups cannot collide through control IDs.
  • A normal view model does not expose or manually maintain IsDirty; whole-value assignment triggers rebuilding.
  • Two views observing the same value-based view model both see an update.
  • Existing low-level drawing and custom hit-area composition remain possible.

Non-goals

  • A retained-mode element tree.
  • Widget reconciliation.
  • Mandatory data binding.
  • A comprehensive theme system before the standard control API is established.

These improvements can be implemented incrementally. Completing Button(), separating visual and interactive rectangles, and introducing scoped control identity are useful independently of the larger layout work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
       blocks
      (function() {
      function addCopyButtons() {
      document.querySelectorAll('pre code').forEach(function(codeBlock) {
      if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
      codeBlock.parentElement.setAttribute('data-copy-added', 'true');
      var btn = document.createElement('button');
      btn.textContent = 'Copy';
      btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
      btn.onmouseover = function() { this.style.opacity = '1'; };
      btn.onmouseout = function() { this.style.opacity = '0.7'; };
      btn.onclick = function() {
      navigator.clipboard.writeText(codeBlock.textContent).then(function() {
      btn.textContent = 'Copied!';
      setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
      });
      };
      codeBlock.parentElement.style.position = 'relative';
      codeBlock.parentElement.appendChild(btn);
      });
      }
      addCopyButtons();
      // Re-run on dynamic content
      var observer = new MutationObserver(addCopyButtons);
      observer.observe(document.body, { childList: true, subtree: true });
      })();
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Improve Pencuil immediate-mode authoring ergonomics #418

      Description

      @botoddly

      Context

      Pencuil's immediate-mode locality is useful, but its current API leaves common control, layout, identity, presentation, and invalidation work to each application.

      The gameplay settings editor in Babka and the Pixely Hotbar tutorial expose the recurring friction:

      • Pencil.Button() registers interaction without drawing the button or advancing layout, so applications build buttons from Panel(), text measurement, and manual centering.
      • Panel() always registers hover and click tests, even when used only as a background or decorative rectangle.
      • Layout frequently requires absolute MoveTo() calls, manual text measurement, coordinate arithmetic, and saving/restoring Pencil cursor state around composed content.
      • Text and number fields accept raw integer IDs from a namespace shared by all views in a Pencuil root.
      • Every IPencuilViewModel exposes mutable IsDirty, and every state mutation must remember to set it.

      Improve Pencuil itself while preserving immediate-mode authoring and rebuild-only-when-needed rendering.

      Proposed improvements

      Complete standard controls

      Make Button() a complete control: draw its background, border, and text; expose its interaction state; support enabled state and fixed or content-derived sizing; and contribute its final bounds to the active layout.

      Separate visuals from interaction

      Use distinct operations for distinct intent:

      • Rectangle() or Fill() for non-interactive visuals;
      • Button() for ordinary button behavior;
      • HitArea() or an equivalent primitive for custom interactive regions such as hotbar slots.

      Drawing a background must not silently register a full-window interactive area.

      Add composable immediate-mode layout

      Add layout scopes such as Row, Column, Padding, Align, and Overlay. A nested scope must correctly contribute its resulting bounds to its parent, so composing content does not require applications to preserve and restore CurrentPosition and CurrentSize.

      The API should cover common fixed-size, content-size, stretch, gap, and alignment cases without introducing a retained element tree.

      Scope control identity

      Control IDs should:

      • be scoped automatically by the current view;
      • accept semantic keys such as enums without casts;
      • support nested scopes for repeated controls;
      • prevent collisions between independently composed views.

      Add presentation conveniences

      Directly support common operations that currently require measurement and coordinate plumbing:

      • horizontal and vertical text alignment;
      • wrapped text constrained to a maximum width;
      • centering inside the current layout region;
      • clipping and scrolling;
      • enabled, hovered, pressed, and selected visual states.

      Applications should rarely call MeasureText() merely to position content.

      Encapsulate dirty tracking

      Keep conditional rebuilding, but remove IsDirty bookkeeping from application state. Provide a value-based wrapper along these lines:

      publicreadonlyrecordstructGameplaySettingsViewState(floatCharacterSpeed,floatSowingDuration);ViewModel<GameplaySettingsViewState>viewModel=new(initialState);viewModel.Value=viewModel.Valuewith{CharacterSpeed=editedSpeed};

      ViewModel<TValue> should require TValue : struct. Assigning Value should invalidate observers, backed by a monotonically increasing version rather than a consumable boolean. Each view tracks its own observed version, allowing multiple views to observe the same model safely.

      Keep Value by value initially for assignment ergonomics. A ref readonly accessor can be considered later only if profiling demonstrates meaningful copies.

      Actions and injected services remain separate from the state value; this is invalidation support, not mandatory MVVM or data binding.

      Acceptance criteria

      • The Babka gameplay settings editor no longer needs its custom button implementation, integer ID casts, manual text centering, or most absolute coordinate arithmetic.
      • The Hotbar tutorial can compose an image inside each slot without saving and restoring Pencil cursor state.
      • Decorative backgrounds do not participate in hit testing.
      • Independently authored views and repeated control groups cannot collide through control IDs.
      • A normal view model does not expose or manually maintain IsDirty; whole-value assignment triggers rebuilding.
      • Two views observing the same value-based view model both see an update.
      • Existing low-level drawing and custom hit-area composition remain possible.

      Non-goals

      • A retained-mode element tree.
      • Widget reconciliation.
      • Mandatory data binding.
      • A comprehensive theme system before the standard control API is established.

      These improvements can be implemented incrementally. Completing Button(), separating visual and interactive rectangles, and introducing scoped control identity are useful independently of the larger layout work.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Improve Pencuil immediate-mode authoring ergonomics #418

          Description

          @botoddly

          Context

          Pencuil's immediate-mode locality is useful, but its current API leaves common control, layout, identity, presentation, and invalidation work to each application.

          The gameplay settings editor in Babka and the Pixely Hotbar tutorial expose the recurring friction:

          • Pencil.Button() registers interaction without drawing the button or advancing layout, so applications build buttons from Panel(), text measurement, and manual centering.
          • Panel() always registers hover and click tests, even when used only as a background or decorative rectangle.
          • Layout frequently requires absolute MoveTo() calls, manual text measurement, coordinate arithmetic, and saving/restoring Pencil cursor state around composed content.
          • Text and number fields accept raw integer IDs from a namespace shared by all views in a Pencuil root.
          • Every IPencuilViewModel exposes mutable IsDirty, and every state mutation must remember to set it.

          Improve Pencuil itself while preserving immediate-mode authoring and rebuild-only-when-needed rendering.

          Proposed improvements

          Complete standard controls

          Make Button() a complete control: draw its background, border, and text; expose its interaction state; support enabled state and fixed or content-derived sizing; and contribute its final bounds to the active layout.

          Separate visuals from interaction

          Use distinct operations for distinct intent:

          • Rectangle() or Fill() for non-interactive visuals;
          • Button() for ordinary button behavior;
          • HitArea() or an equivalent primitive for custom interactive regions such as hotbar slots.

          Drawing a background must not silently register a full-window interactive area.

          Add composable immediate-mode layout

          Add layout scopes such as Row, Column, Padding, Align, and Overlay. A nested scope must correctly contribute its resulting bounds to its parent, so composing content does not require applications to preserve and restore CurrentPosition and CurrentSize.

          The API should cover common fixed-size, content-size, stretch, gap, and alignment cases without introducing a retained element tree.

          Scope control identity

          Control IDs should:

          • be scoped automatically by the current view;
          • accept semantic keys such as enums without casts;
          • support nested scopes for repeated controls;
          • prevent collisions between independently composed views.

          Add presentation conveniences

          Directly support common operations that currently require measurement and coordinate plumbing:

          • horizontal and vertical text alignment;
          • wrapped text constrained to a maximum width;
          • centering inside the current layout region;
          • clipping and scrolling;
          • enabled, hovered, pressed, and selected visual states.

          Applications should rarely call MeasureText() merely to position content.

          Encapsulate dirty tracking

          Keep conditional rebuilding, but remove IsDirty bookkeeping from application state. Provide a value-based wrapper along these lines:

          publicreadonlyrecordstructGameplaySettingsViewState(floatCharacterSpeed,floatSowingDuration);ViewModel<GameplaySettingsViewState>viewModel=new(initialState);viewModel.Value=viewModel.Valuewith{CharacterSpeed=editedSpeed};

          ViewModel<TValue> should require TValue : struct. Assigning Value should invalidate observers, backed by a monotonically increasing version rather than a consumable boolean. Each view tracks its own observed version, allowing multiple views to observe the same model safely.

          Keep Value by value initially for assignment ergonomics. A ref readonly accessor can be considered later only if profiling demonstrates meaningful copies.

          Actions and injected services remain separate from the state value; this is invalidation support, not mandatory MVVM or data binding.

          Acceptance criteria

          • The Babka gameplay settings editor no longer needs its custom button implementation, integer ID casts, manual text centering, or most absolute coordinate arithmetic.
          • The Hotbar tutorial can compose an image inside each slot without saving and restoring Pencil cursor state.
          • Decorative backgrounds do not participate in hit testing.
          • Independently authored views and repeated control groups cannot collide through control IDs.
          • A normal view model does not expose or manually maintain IsDirty; whole-value assignment triggers rebuilding.
          • Two views observing the same value-based view model both see an update.
          • Existing low-level drawing and custom hit-area composition remain possible.

          Non-goals

          • A retained-mode element tree.
          • Widget reconciliation.
          • Mandatory data binding.
          • A comprehensive theme system before the standard control API is established.

          These improvements can be implemented incrementally. Completing Button(), separating visual and interactive rectangles, and introducing scoped control identity are useful independently of the larger layout work.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Improve Pencuil immediate-mode authoring ergonomics #418

              Description

              @botoddly

              Context

              Pencuil's immediate-mode locality is useful, but its current API leaves common control, layout, identity, presentation, and invalidation work to each application.

              The gameplay settings editor in Babka and the Pixely Hotbar tutorial expose the recurring friction:

              • Pencil.Button() registers interaction without drawing the button or advancing layout, so applications build buttons from Panel(), text measurement, and manual centering.
              • Panel() always registers hover and click tests, even when used only as a background or decorative rectangle.
              • Layout frequently requires absolute MoveTo() calls, manual text measurement, coordinate arithmetic, and saving/restoring Pencil cursor state around composed content.
              • Text and number fields accept raw integer IDs from a namespace shared by all views in a Pencuil root.
              • Every IPencuilViewModel exposes mutable IsDirty, and every state mutation must remember to set it.

              Improve Pencuil itself while preserving immediate-mode authoring and rebuild-only-when-needed rendering.

              Proposed improvements

              Complete standard controls

              Make Button() a complete control: draw its background, border, and text; expose its interaction state; support enabled state and fixed or content-derived sizing; and contribute its final bounds to the active layout.

              Separate visuals from interaction

              Use distinct operations for distinct intent:

              • Rectangle() or Fill() for non-interactive visuals;
              • Button() for ordinary button behavior;
              • HitArea() or an equivalent primitive for custom interactive regions such as hotbar slots.

              Drawing a background must not silently register a full-window interactive area.

              Add composable immediate-mode layout

              Add layout scopes such as Row, Column, Padding, Align, and Overlay. A nested scope must correctly contribute its resulting bounds to its parent, so composing content does not require applications to preserve and restore CurrentPosition and CurrentSize.

              The API should cover common fixed-size, content-size, stretch, gap, and alignment cases without introducing a retained element tree.

              Scope control identity

              Control IDs should:

              • be scoped automatically by the current view;
              • accept semantic keys such as enums without casts;
              • support nested scopes for repeated controls;
              • prevent collisions between independently composed views.

              Add presentation conveniences

              Directly support common operations that currently require measurement and coordinate plumbing:

              • horizontal and vertical text alignment;
              • wrapped text constrained to a maximum width;
              • centering inside the current layout region;
              • clipping and scrolling;
              • enabled, hovered, pressed, and selected visual states.

              Applications should rarely call MeasureText() merely to position content.

              Encapsulate dirty tracking

              Keep conditional rebuilding, but remove IsDirty bookkeeping from application state. Provide a value-based wrapper along these lines:

              publicreadonlyrecordstructGameplaySettingsViewState(floatCharacterSpeed,floatSowingDuration);ViewModel<GameplaySettingsViewState>viewModel=new(initialState);viewModel.Value=viewModel.Valuewith{CharacterSpeed=editedSpeed};

              ViewModel<TValue> should require TValue : struct. Assigning Value should invalidate observers, backed by a monotonically increasing version rather than a consumable boolean. Each view tracks its own observed version, allowing multiple views to observe the same model safely.

              Keep Value by value initially for assignment ergonomics. A ref readonly accessor can be considered later only if profiling demonstrates meaningful copies.

              Actions and injected services remain separate from the state value; this is invalidation support, not mandatory MVVM or data binding.

              Acceptance criteria

              • The Babka gameplay settings editor no longer needs its custom button implementation, integer ID casts, manual text centering, or most absolute coordinate arithmetic.
              • The Hotbar tutorial can compose an image inside each slot without saving and restoring Pencil cursor state.
              • Decorative backgrounds do not participate in hit testing.
              • Independently authored views and repeated control groups cannot collide through control IDs.
              • A normal view model does not expose or manually maintain IsDirty; whole-value assignment triggers rebuilding.
              • Two views observing the same value-based view model both see an update.
              • Existing low-level drawing and custom hit-area composition remain possible.

              Non-goals

              • A retained-mode element tree.
              • Widget reconciliation.
              • Mandatory data binding.
              • A comprehensive theme system before the standard control API is established.

              These improvements can be implemented incrementally. Completing Button(), separating visual and interactive rectangles, and introducing scoped control identity are useful independently of the larger layout work.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Improve Pencuil immediate-mode authoring ergonomics #418

                  Description

                  @botoddly

                  Context

                  Pencuil's immediate-mode locality is useful, but its current API leaves common control, layout, identity, presentation, and invalidation work to each application.

                  The gameplay settings editor in Babka and the Pixely Hotbar tutorial expose the recurring friction:

                  • Pencil.Button() registers interaction without drawing the button or advancing layout, so applications build buttons from Panel(), text measurement, and manual centering.
                  • Panel() always registers hover and click tests, even when used only as a background or decorative rectangle.
                  • Layout frequently requires absolute MoveTo() calls, manual text measurement, coordinate arithmetic, and saving/restoring Pencil cursor state around composed content.
                  • Text and number fields accept raw integer IDs from a namespace shared by all views in a Pencuil root.
                  • Every IPencuilViewModel exposes mutable IsDirty, and every state mutation must remember to set it.

                  Improve Pencuil itself while preserving immediate-mode authoring and rebuild-only-when-needed rendering.

                  Proposed improvements

                  Complete standard controls

                  Make Button() a complete control: draw its background, border, and text; expose its interaction state; support enabled state and fixed or content-derived sizing; and contribute its final bounds to the active layout.

                  Separate visuals from interaction

                  Use distinct operations for distinct intent:

                  • Rectangle() or Fill() for non-interactive visuals;
                  • Button() for ordinary button behavior;
                  • HitArea() or an equivalent primitive for custom interactive regions such as hotbar slots.

                  Drawing a background must not silently register a full-window interactive area.

                  Add composable immediate-mode layout

                  Add layout scopes such as Row, Column, Padding, Align, and Overlay. A nested scope must correctly contribute its resulting bounds to its parent, so composing content does not require applications to preserve and restore CurrentPosition and CurrentSize.

                  The API should cover common fixed-size, content-size, stretch, gap, and alignment cases without introducing a retained element tree.

                  Scope control identity

                  Control IDs should:

                  • be scoped automatically by the current view;
                  • accept semantic keys such as enums without casts;
                  • support nested scopes for repeated controls;
                  • prevent collisions between independently composed views.

                  Add presentation conveniences

                  Directly support common operations that currently require measurement and coordinate plumbing:

                  • horizontal and vertical text alignment;
                  • wrapped text constrained to a maximum width;
                  • centering inside the current layout region;
                  • clipping and scrolling;
                  • enabled, hovered, pressed, and selected visual states.

                  Applications should rarely call MeasureText() merely to position content.

                  Encapsulate dirty tracking

                  Keep conditional rebuilding, but remove IsDirty bookkeeping from application state. Provide a value-based wrapper along these lines:

                  publicreadonlyrecordstructGameplaySettingsViewState(floatCharacterSpeed,floatSowingDuration);ViewModel<GameplaySettingsViewState>viewModel=new(initialState);viewModel.Value=viewModel.Valuewith{CharacterSpeed=editedSpeed};

                  ViewModel<TValue> should require TValue : struct. Assigning Value should invalidate observers, backed by a monotonically increasing version rather than a consumable boolean. Each view tracks its own observed version, allowing multiple views to observe the same model safely.

                  Keep Value by value initially for assignment ergonomics. A ref readonly accessor can be considered later only if profiling demonstrates meaningful copies.

                  Actions and injected services remain separate from the state value; this is invalidation support, not mandatory MVVM or data binding.

                  Acceptance criteria

                  • The Babka gameplay settings editor no longer needs its custom button implementation, integer ID casts, manual text centering, or most absolute coordinate arithmetic.
                  • The Hotbar tutorial can compose an image inside each slot without saving and restoring Pencil cursor state.
                  • Decorative backgrounds do not participate in hit testing.
                  • Independently authored views and repeated control groups cannot collide through control IDs.
                  • A normal view model does not expose or manually maintain IsDirty; whole-value assignment triggers rebuilding.
                  • Two views observing the same value-based view model both see an update.
                  • Existing low-level drawing and custom hit-area composition remain possible.

                  Non-goals

                  • A retained-mode element tree.
                  • Widget reconciliation.
                  • Mandatory data binding.
                  • A comprehensive theme system before the standard control API is established.

                  These improvements can be implemented incrementally. Completing Button(), separating visual and interactive rectangles, and introducing scoped control identity are useful independently of the larger layout work.

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Improve Pencuil immediate-mode authoring ergonomics #418

                      Description

                      @botoddly

                      Context

                      Pencuil's immediate-mode locality is useful, but its current API leaves common control, layout, identity, presentation, and invalidation work to each application.

                      The gameplay settings editor in Babka and the Pixely Hotbar tutorial expose the recurring friction:

                      • Pencil.Button() registers interaction without drawing the button or advancing layout, so applications build buttons from Panel(), text measurement, and manual centering.
                      • Panel() always registers hover and click tests, even when used only as a background or decorative rectangle.
                      • Layout frequently requires absolute MoveTo() calls, manual text measurement, coordinate arithmetic, and saving/restoring Pencil cursor state around composed content.
                      • Text and number fields accept raw integer IDs from a namespace shared by all views in a Pencuil root.
                      • Every IPencuilViewModel exposes mutable IsDirty, and every state mutation must remember to set it.

                      Improve Pencuil itself while preserving immediate-mode authoring and rebuild-only-when-needed rendering.

                      Proposed improvements

                      Complete standard controls

                      Make Button() a complete control: draw its background, border, and text; expose its interaction state; support enabled state and fixed or content-derived sizing; and contribute its final bounds to the active layout.

                      Separate visuals from interaction

                      Use distinct operations for distinct intent:

                      • Rectangle() or Fill() for non-interactive visuals;
                      • Button() for ordinary button behavior;
                      • HitArea() or an equivalent primitive for custom interactive regions such as hotbar slots.

                      Drawing a background must not silently register a full-window interactive area.

                      Add composable immediate-mode layout

                      Add layout scopes such as Row, Column, Padding, Align, and Overlay. A nested scope must correctly contribute its resulting bounds to its parent, so composing content does not require applications to preserve and restore CurrentPosition and CurrentSize.

                      The API should cover common fixed-size, content-size, stretch, gap, and alignment cases without introducing a retained element tree.

                      Scope control identity

                      Control IDs should:

                      • be scoped automatically by the current view;
                      • accept semantic keys such as enums without casts;
                      • support nested scopes for repeated controls;
                      • prevent collisions between independently composed views.

                      Add presentation conveniences

                      Directly support common operations that currently require measurement and coordinate plumbing:

                      • horizontal and vertical text alignment;
                      • wrapped text constrained to a maximum width;
                      • centering inside the current layout region;
                      • clipping and scrolling;
                      • enabled, hovered, pressed, and selected visual states.

                      Applications should rarely call MeasureText() merely to position content.

                      Encapsulate dirty tracking

                      Keep conditional rebuilding, but remove IsDirty bookkeeping from application state. Provide a value-based wrapper along these lines:

                      publicreadonlyrecordstructGameplaySettingsViewState(floatCharacterSpeed,floatSowingDuration);ViewModel<GameplaySettingsViewState>viewModel=new(initialState);viewModel.Value=viewModel.Valuewith{CharacterSpeed=editedSpeed};

                      ViewModel<TValue> should require TValue : struct. Assigning Value should invalidate observers, backed by a monotonically increasing version rather than a consumable boolean. Each view tracks its own observed version, allowing multiple views to observe the same model safely.

                      Keep Value by value initially for assignment ergonomics. A ref readonly accessor can be considered later only if profiling demonstrates meaningful copies.

                      Actions and injected services remain separate from the state value; this is invalidation support, not mandatory MVVM or data binding.

                      Acceptance criteria

                      • The Babka gameplay settings editor no longer needs its custom button implementation, integer ID casts, manual text centering, or most absolute coordinate arithmetic.
                      • The Hotbar tutorial can compose an image inside each slot without saving and restoring Pencil cursor state.
                      • Decorative backgrounds do not participate in hit testing.
                      • Independently authored views and repeated control groups cannot collide through control IDs.
                      • A normal view model does not expose or manually maintain IsDirty; whole-value assignment triggers rebuilding.
                      • Two views observing the same value-based view model both see an update.
                      • Existing low-level drawing and custom hit-area composition remain possible.

                      Non-goals

                      • A retained-mode element tree.
                      • Widget reconciliation.
                      • Mandatory data binding.
                      • A comprehensive theme system before the standard control API is established.

                      These improvements can be implemented incrementally. Completing Button(), separating visual and interactive rectangles, and introducing scoped control identity are useful independently of the larger layout work.

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Improve Pencuil immediate-mode authoring ergonomics #418

                          Description

                          @botoddly

                          Context

                          Pencuil's immediate-mode locality is useful, but its current API leaves common control, layout, identity, presentation, and invalidation work to each application.

                          The gameplay settings editor in Babka and the Pixely Hotbar tutorial expose the recurring friction:

                          • Pencil.Button() registers interaction without drawing the button or advancing layout, so applications build buttons from Panel(), text measurement, and manual centering.
                          • Panel() always registers hover and click tests, even when used only as a background or decorative rectangle.
                          • Layout frequently requires absolute MoveTo() calls, manual text measurement, coordinate arithmetic, and saving/restoring Pencil cursor state around composed content.
                          • Text and number fields accept raw integer IDs from a namespace shared by all views in a Pencuil root.
                          • Every IPencuilViewModel exposes mutable IsDirty, and every state mutation must remember to set it.

                          Improve Pencuil itself while preserving immediate-mode authoring and rebuild-only-when-needed rendering.

                          Proposed improvements

                          Complete standard controls

                          Make Button() a complete control: draw its background, border, and text; expose its interaction state; support enabled state and fixed or content-derived sizing; and contribute its final bounds to the active layout.

                          Separate visuals from interaction

                          Use distinct operations for distinct intent:

                          • Rectangle() or Fill() for non-interactive visuals;
                          • Button() for ordinary button behavior;
                          • HitArea() or an equivalent primitive for custom interactive regions such as hotbar slots.

                          Drawing a background must not silently register a full-window interactive area.

                          Add composable immediate-mode layout

                          Add layout scopes such as Row, Column, Padding, Align, and Overlay. A nested scope must correctly contribute its resulting bounds to its parent, so composing content does not require applications to preserve and restore CurrentPosition and CurrentSize.

                          The API should cover common fixed-size, content-size, stretch, gap, and alignment cases without introducing a retained element tree.

                          Scope control identity

                          Control IDs should:

                          • be scoped automatically by the current view;
                          • accept semantic keys such as enums without casts;
                          • support nested scopes for repeated controls;
                          • prevent collisions between independently composed views.

                          Add presentation conveniences

                          Directly support common operations that currently require measurement and coordinate plumbing:

                          • horizontal and vertical text alignment;
                          • wrapped text constrained to a maximum width;
                          • centering inside the current layout region;
                          • clipping and scrolling;
                          • enabled, hovered, pressed, and selected visual states.

                          Applications should rarely call MeasureText() merely to position content.

                          Encapsulate dirty tracking

                          Keep conditional rebuilding, but remove IsDirty bookkeeping from application state. Provide a value-based wrapper along these lines:

                          publicreadonlyrecordstructGameplaySettingsViewState(floatCharacterSpeed,floatSowingDuration);ViewModel<GameplaySettingsViewState>viewModel=new(initialState);viewModel.Value=viewModel.Valuewith{CharacterSpeed=editedSpeed};

                          ViewModel<TValue> should require TValue : struct. Assigning Value should invalidate observers, backed by a monotonically increasing version rather than a consumable boolean. Each view tracks its own observed version, allowing multiple views to observe the same model safely.

                          Keep Value by value initially for assignment ergonomics. A ref readonly accessor can be considered later only if profiling demonstrates meaningful copies.

                          Actions and injected services remain separate from the state value; this is invalidation support, not mandatory MVVM or data binding.

                          Acceptance criteria

                          • The Babka gameplay settings editor no longer needs its custom button implementation, integer ID casts, manual text centering, or most absolute coordinate arithmetic.
                          • The Hotbar tutorial can compose an image inside each slot without saving and restoring Pencil cursor state.
                          • Decorative backgrounds do not participate in hit testing.
                          • Independently authored views and repeated control groups cannot collide through control IDs.
                          • A normal view model does not expose or manually maintain IsDirty; whole-value assignment triggers rebuilding.
                          • Two views observing the same value-based view model both see an update.
                          • Existing low-level drawing and custom hit-area composition remain possible.

                          Non-goals

                          • A retained-mode element tree.
                          • Widget reconciliation.
                          • Mandatory data binding.
                          • A comprehensive theme system before the standard control API is established.

                          These improvements can be implemented incrementally. Completing Button(), separating visual and interactive rectangles, and introducing scoped control identity are useful independently of the larger layout work.

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Improve Pencuil immediate-mode authoring ergonomics #418

                              Description

                              @botoddly

                              Context

                              Pencuil's immediate-mode locality is useful, but its current API leaves common control, layout, identity, presentation, and invalidation work to each application.

                              The gameplay settings editor in Babka and the Pixely Hotbar tutorial expose the recurring friction:

                              • Pencil.Button() registers interaction without drawing the button or advancing layout, so applications build buttons from Panel(), text measurement, and manual centering.
                              • Panel() always registers hover and click tests, even when used only as a background or decorative rectangle.
                              • Layout frequently requires absolute MoveTo() calls, manual text measurement, coordinate arithmetic, and saving/restoring Pencil cursor state around composed content.
                              • Text and number fields accept raw integer IDs from a namespace shared by all views in a Pencuil root.
                              • Every IPencuilViewModel exposes mutable IsDirty, and every state mutation must remember to set it.

                              Improve Pencuil itself while preserving immediate-mode authoring and rebuild-only-when-needed rendering.

                              Proposed improvements

                              Complete standard controls

                              Make Button() a complete control: draw its background, border, and text; expose its interaction state; support enabled state and fixed or content-derived sizing; and contribute its final bounds to the active layout.

                              Separate visuals from interaction

                              Use distinct operations for distinct intent:

                              • Rectangle() or Fill() for non-interactive visuals;
                              • Button() for ordinary button behavior;
                              • HitArea() or an equivalent primitive for custom interactive regions such as hotbar slots.

                              Drawing a background must not silently register a full-window interactive area.

                              Add composable immediate-mode layout

                              Add layout scopes such as Row, Column, Padding, Align, and Overlay. A nested scope must correctly contribute its resulting bounds to its parent, so composing content does not require applications to preserve and restore CurrentPosition and CurrentSize.

                              The API should cover common fixed-size, content-size, stretch, gap, and alignment cases without introducing a retained element tree.

                              Scope control identity

                              Control IDs should:

                              • be scoped automatically by the current view;
                              • accept semantic keys such as enums without casts;
                              • support nested scopes for repeated controls;
                              • prevent collisions between independently composed views.

                              Add presentation conveniences

                              Directly support common operations that currently require measurement and coordinate plumbing:

                              • horizontal and vertical text alignment;
                              • wrapped text constrained to a maximum width;
                              • centering inside the current layout region;
                              • clipping and scrolling;
                              • enabled, hovered, pressed, and selected visual states.

                              Applications should rarely call MeasureText() merely to position content.

                              Encapsulate dirty tracking

                              Keep conditional rebuilding, but remove IsDirty bookkeeping from application state. Provide a value-based wrapper along these lines:

                              publicreadonlyrecordstructGameplaySettingsViewState(floatCharacterSpeed,floatSowingDuration);ViewModel<GameplaySettingsViewState>viewModel=new(initialState);viewModel.Value=viewModel.Valuewith{CharacterSpeed=editedSpeed};

                              ViewModel<TValue> should require TValue : struct. Assigning Value should invalidate observers, backed by a monotonically increasing version rather than a consumable boolean. Each view tracks its own observed version, allowing multiple views to observe the same model safely.

                              Keep Value by value initially for assignment ergonomics. A ref readonly accessor can be considered later only if profiling demonstrates meaningful copies.

                              Actions and injected services remain separate from the state value; this is invalidation support, not mandatory MVVM or data binding.

                              Acceptance criteria

                              • The Babka gameplay settings editor no longer needs its custom button implementation, integer ID casts, manual text centering, or most absolute coordinate arithmetic.
                              • The Hotbar tutorial can compose an image inside each slot without saving and restoring Pencil cursor state.
                              • Decorative backgrounds do not participate in hit testing.
                              • Independently authored views and repeated control groups cannot collide through control IDs.
                              • A normal view model does not expose or manually maintain IsDirty; whole-value assignment triggers rebuilding.
                              • Two views observing the same value-based view model both see an update.
                              • Existing low-level drawing and custom hit-area composition remain possible.

                              Non-goals

                              • A retained-mode element tree.
                              • Widget reconciliation.
                              • Mandatory data binding.
                              • A comprehensive theme system before the standard control API is established.

                              These improvements can be implemented incrementally. Completing Button(), separating visual and interactive rectangles, and introducing scoped control identity are useful independently of the larger layout work.

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions