Files
ARCADIA-Studio/harness/experiments/palette-readability-20260913/palette-selection-remedy-20260912.md

15 KiB

Source-editor selection remedy and centralized control styling

2026-09-12. Bounded source investigation for root-owned issue #26, following the independent palette critique. No production edit, compiler, native UI interaction, or Gitea mutation. The dependency-free calculator was executed successfully; its JSON receipt records candidate ratios and hashes of the actual source/API inputs. Neither candidate has been rendered or accepted.

The simplest feasible color remedy is one explicit source-editor style plus a static dark syntax palette. It avoids selection-coordinate tracking, custom rendering and a dependency fork. Prefer the concrete S2 candidate below for the first native trial: a white editor surface, medium selection fill #80958B, and the seven dark syntax colors listed below. White is already an established application surface. It permits somewhat brighter token colors than preserving current BG at the same selection/foreground benchmarks.

Use Theme::custom_with_fn centrally for ordinary generated control colors, where the roles are shared safely. Keep the source editor's special surface/selection/value in one .style(style::source_editor) adapter. This hybrid is smaller than putting bespoke adapters on every Inspector field and avoids forcing source-editor contrast constraints onto all shared theme pairs. It is a refinement of the earlier audit's suggested implementation boundary.

This resolves the color/API feasibility gap for selections that Iced actually paints. Existing newline-only selection geometry and loss of selection paint on blur remain separate limitations below; do not claim a palette guarantees every selected range is always visible.

Concrete static candidates

The engineering targets here are 3:1 for the selection fill against its actual adjacent editor surface and 4.5:1 for every glyph color against both surfaces. These are explicit candidate criteria, not a claim that WCAG universally requires 3:1 between every state. Syntax class identity, lexer spans, text, monospace font and size remain unchanged. Colors darken existing purple/blue/green/brown/red families; they do not neutralize diagram category colors.

Candidate Normal editor surface Selection fill Fill/surface Minimum selected glyph contrast Minimum normal glyph contrast
S1: preserve current background source floats (.945,.956,.969) #7A8C85 3.210398 4.723634 15.164746
S2: white source surface #FFFFFF #80958B 3.186824 4.835525 15.409967

S2's exact proposed formatter/value colors:

Role Color Normal surface contrast Selection contrast
Keyword #331748 15.476501 4.856403
Identifier #04273D 15.409967 4.835525
String #042A1B 15.530540 4.873360
Number #3F1A06 15.421448 4.839128
Comment #212529 15.426285 4.840646
Error token #4C0914 15.481290 4.857906
Punctuation/whitespace #20252C 15.413837 4.836740
Widget value fallback and caret #212529 15.426285 4.840646

The fallback is essential: A's ordinary ink #202C3A is only 4.444090:1 on S2 selection (3.990758 on S1). Assigning selection fill while leaving all other editor defaults unchanged does not implement this candidate. Use the same dark fallback for any visible unformatted text, and explicitly choose a readable placeholder. Leave font mapping and lexer behavior intact.

The candidate generation scaled each current token's linear RGB channels to target relative luminance 0.018 (S2) or 0.012 (S1), then rounded to the listed byte colors. Ratios are recomputed from those final bytes. This keeps approximate linear chromaticity before rounding; it does not prove perceived hue or scanability is preserved on a limited-color display. Production needs only the final constants, not runtime color derivation.

The tradeoff is inherent. For dark text, an intermediate darker selection and a lighter normal surface, their contrast ratios multiply. Requiring selection/surface >=3 and text/selection >=4.5 implies text/normal >=13.5. Consequently a static palette satisfying both benchmarks has very dark syntax colors. A native trial must establish whether the remaining token color distinctions help enough. The higher normal contrast is readable arithmetic, not evidence that the syntax appearance is optimal.

Actual public API path

views.rs:1187 already constructs a single text_editor(&app.source_editor) and applies highlight_with::<ArcadiaHighlighter>(app.source_snapshot.clone(), highlight::format). Installed iced_widget-0.14.2/src/text_editor.rs:323 exposes .style(impl Fn(&Theme, Status) -> Style). Style exposes background, border, placeholder, value and selection. A small function in style.rs can start from text_editor::default(theme, status), set S2 background/value/placeholder/selection, and preserve or explicitly set the existing status-dependent border. The exact implementation should ensure a focused border is still visible against the chosen white surface.

highlight.rs:62 directly maps all token classes to Format { color: Some(...), font: None }. Replacing those seven colors implements the static syntax half. No change to ArcadiaHighlighter::Settings, tokenization, source snapshots, SourceEdit, selection actions or file persistence is needed. The selected and normal foregrounds are the same, so blur, selection reversal and rerender cannot leave a stale light foreground on a light background. Source code and graph category coloring remain independent.

This is a source-feasible sketch, not compiled proof. The normal integration gate must still exercise actual types and resolved styles. Use a dedicated source selection token rather than overwriting navigation's much lighter selection fill; those roles have different adjacent surfaces and content.

Why range-aware selected foreground is not the first implementation

There is a partial public path: Highlighter::Settings is any PartialEq + Clone type, and Highlight is unconstrained (iced_core-0.14.0/src/text/highlighter.rs). Settings could carry a source snapshot and selection; token spans could be split into selected/unselected fragments. A richer Highlight would let the formatter return white selected glyphs over an accent selection fill. text_editor.rs:637 calls update when settings change, and the graphics editor formats the resulting spans. Numerically white on A accent #00665C is 6.873775:1, while the fill against current BG is 6.218258:1. It would preserve ordinary syntax colors outside selection and temporarily suppress hue within selected text.

However, the current API does not provide a simple, complete logical selected-range contract to feed that mechanism:

  • Content::cursor() (text_editor.rs:426) returns Cursor { position, selection }. In iced_graphics-0.14.0/src/text/editor.rs:277, its selection comes from the raw cosmic-text Normal/Word/Line anchor, with the kind discarded. Columns are UTF-8 byte indexes, not character counts (cosmic-text-0.15.0/src/cursor.rs).
  • Actual selection painting uses cosmic-text selection_bounds(), which expands Word and Line selections (cosmic-text/src/edit/mod.rs:217). Double-click sets Selection::Word(self.cursor); triple-click sets Line. Thus raw position and anchor can be identical while a complete word or line is painted selected. Sorting the exposed endpoints would incorrectly produce an empty highlight range.
  • Content::selection() returns copied text, not canonical range endpoints or selection kind. Searching that string in source is ambiguous for repeated fragments. Duplicating Unicode word/line expansion and remembering every action would introduce a second selection implementation and lifecycle risks.
  • The range fill is drawn only while the widget is focused (text_editor.rs:1021). White selected-token formatting that remains after blur would become unreadable. Highlighter formatting receives class/theme, not widget status. Widget State has focus information, but Content does not expose it as a simple synchronized view setting. A custom operation/wrapper could acquire more state, at additional timing and invalidation cost.
  • Selection-only setting changes would require rehighlighting affected spans. The present highlighter update rebuilds tokenized lines; simply extending its settings would retokenize the source during selection drags. A correct optimized version would separate token cache invalidation from selection invalidation and verify changes both entering and leaving a range, including offscreen lines. That is unnecessary work for the static candidate.

These are concrete correctness/performance obligations, not a claim that range-aware formatting is impossible without a fork. Prefer it only if the static candidate fails the color-class/native reading task and the additional selection/focus contract can be supplied without copying editor behavior.

A third small option is to switch all syntax colors to the dark palette whenever a selection exists, returning to stronger original hues afterward. It avoids exact selected endpoints but changes unselected text too, creates visible palette changes during selection, and adds settings invalidation without improving selected-text color distinction. It is a possible later experiment, not the preferred first implementation.

Palette cannot repair absent selection geometry

iced_graphics/src/text/editor.rs:781 computes range widths from glyph ranges. Empty ranges yield width zero; selection() drops zero-width rectangles. A selection containing only a line ending can therefore have no painted range quads, while the widget is in the Range branch and draws no caret. Empty intermediate lines also lack fill. This is a source-derived prediction, not a witnessed native failure in this task.

Keep newline-only, empty-line and trailing-whitespace cases in the native evaluator. If the requirement is that every selection—including only a newline—must be visible, neither S1 nor S2 fulfills that universally through colors alone. Preserve a separate geometry finding and assess an upstream/small rendering extension against an actual witness; do not add a fake outline to an API that cannot draw it. Losing the selection fill when focus leaves the editor is also existing behavior; verify that refocusing restores the range and that copy/edit operations remain understandable.

Centralized Extended palette: feasible with a small hybrid boundary

Installed iced_core-0.14.0/src/theme.rs:100 exposes:

Theme::custom_with_fn(name, palette, |palette| {
    let mut extended = iced::theme::palette::Extended::generate(palette);
    // Set reviewed shared pairs here; return extended.
    extended
})

Extended, its nested role fields and Pair { color, text } are public. Pair::new(color, text) uses Iced's readable-text adjustment; a direct Pair literal preserves exact values but requires explicit contrast verification. No custom Theme type or theme framework is needed. The generator runs at theme creation and existing default controls read the resulting extended palette.

Extended role Actual consumers / coupled obligations
background.base Text-input/editor background and fallback text; unchecked active checkbox background. Keep the pair coherent. Source S2 overrides its own surface/value.
background.weak Pick-list and popup-menu fill/text, disabled-input fill, hovered unchecked checkbox fill, scrollbar rail. It is a surface, not merely a faint border token.
background.strong.color Ordinary input/editor/pick-list/menu border, checkbox outline; also disabled checked-checkbox fill and some button outlines. Darkening it is feasible but has those extra effects.
secondary.base.color Input/editor/pick-list placeholder; also secondary-button background. Set its .text coherently and inspect actual buttons.
primary.weak.color Default input/editor selection fill. Those widgets do not consume the pair's .text as selected foreground. Source S2 should override this role locally.
primary.strong Focus/opened borders, primary-button hover fill and menu selected background/text. Menu uses the strong pair's text; hovered primary buttons retain base text on strong fill.

Evidence: text_input.rs:1758, text_editor.rs:1466, pick_list.rs:904, overlay/menu.rs:645, button.rs:599,612, checkbox.rs:569 in installed iced_widget 0.14.2. Checked hover checkboxes also mix primary base text with strong fill. Pair readability in isolation does not prove these cross-state combinations.

Recommendation: centralize shared opaque surface, secondary text, boundary and interaction pairs in style::theme(); retain one source-editor override; add another local adapter only when a genuinely incompatible consumer is found. Do not globally darken background.weak as a border fix, and do not assume setting primary.weak.text repairs input selection. Ordinary input value/selection pairs still need their own resolved-color check; S2's token colors must not be copied into every control by accident.

Native and integration acceptance cases

  1. Resolve actual production source-editor styles for active/hover/focused/disabled states and all seven actual formatter outputs. Check S2's stated surfaces and ratios without rounding. Include the wrong A-ink fallback as a negative control. Verify any centralized theme's default input, pick-list, menu, checkbox and button cross-state pairs, not just Pair objects.
  2. Open a representative source file at the actual 14 px monospace setting, native minimum window and ordinary working size/DPI. Read and edit a declaration, string, number and multi-line comment before and during selection. Judge whether near-black category colors still aid scanning; preserve diagnostic text/error identification independently of red hue.
  3. Select part of a token, a whole word by double-click, a line by triple-click, and multiple lines in both directions. Include non-ASCII identifiers, combining characters, tabs, wrapped lines, empty lines, newline-only selection and trailing whitespace. Verify the visible selection agrees with copied text and actual replacement, recording geometry limitations rather than hiding them.
  4. Extend selection with the keyboard, scroll it out and back, transfer focus to another control and return, then copy/paste, undo/redo and switch source files. Verify no stale token attributes, missing text or unintended source edits. A static palette should introduce no selection-specific retokenization or extra source invalidations.
  5. Use original native screenshots plus separately labeled luminance-grayscale/degradation analysis; check range boundaries, glyph readability and focus separately. Record source/build, DPI, window size and analysis parameters. A numerical pass is not acceptance on the user's monitor.

S2 is a concrete, small candidate for the next authorized production/native cycle. Its numerical feasibility is established; its visual quality, source-control integration and native behavior remain unverified.