feat(compose): paragraph alignment #198

Merged
JMR-dev merged 2 commits from feat-76-paragraph-alignment into main 2026-07-03 05:08:41 +00:00
JMR-dev commented 2026-07-03 04:52:19 +00:00 (Migrated from github.com)

Summary

Adds a three-state paragraph-alignment control (start / center / end) to the compose formatting toolbar.

  • Ops (richtext/RichTextEditing.kt): setAlignment(content, start, end, align) + alignmentAt(...) with paragraph-range bookkeeping, mirroring toggleBlock.
    • setAlignment marks every line the selection touches and leaves untouched paragraphs alone. START (the writing-direction default) is stored as no alignment — the range is dropped — so an otherwise-plain paragraph stays plaintext-only; CENTER/END become explicit ranges.
    • Output is the same canonical form RichTextHtml.fromHtml returns: one merged range per run of adjacent same-aligned lines, and blank paragraphs anchor no range (the HTML model can't pin text-align to an empty <p>). A unit test asserts setAlignment output is a serialize→parse fixpoint, so model / HTML / editor rendering never drift.
    • alignmentAt returns the shared alignment (START default for plain paragraphs, null when mixed), driving the control's three-state highlight directly.
  • Control (ui/compose/format/ParagraphAlignmentControl.kt): three glyph buttons (⇤ / ↔ / ⇥), each carrying its accessible action via onClickLabel like the toolbar's other buttons. Self-contained and testable in isolation, mirroring FontSizePicker.
  • Toolbar placement: appended at the end of the toolbar, after the block/link buttons and the FontSizePicker. This applies the #73 lesson — the toolbar overflows and scrolls horizontally, and ComposeScreenTest's no-scroll performClick("•") needs the bullet/numbered/quote buttons to keep their exact on-screen positions, so no new control may be inserted before them.
  • Rendering: applyAlignment routes through the existing ParagraphStyle(textAlign) mapping (already present from the foundation), preserving the selection since alignment never changes the text.

Rendering note

In-editor rendering uses the foundation's existing ParagraphStyle(textAlign) path (model↔AnnotatedString round-trip is unit-tested). The model and its HTML are the source of truth; if on-device ParagraphStyle rendering misbehaves in the field, the model/HTML stay correct — nothing is faked.

Test plan

  • :app:assembleDebug
  • :app:testDebugUnitTest — RichTextEditingTest: setAlignment/alignmentAt across caret, multi-paragraph merge, mid-block split, untouched paragraphs, blank lines, empty document, and mixed selections, plus a serialize→parse round-trip fixpoint; RichTextEditorTest: applyAlignment (centers + keeps selection, START clears).
  • :app:lintDebug
  • :app:ktlintCheck :app:detekt
  • :app:compileDebugAndroidTestKotlin — new compile-only ParagraphAlignmentControlTest drives the control in isolation (glyphs shown; each tap reports its RichAlign).

Emulator E2E left to CI.

Closes #76

🤖 Generated with Claude Code

## Summary Adds a three-state paragraph-alignment control (start / center / end) to the compose formatting toolbar. - **Ops** (`richtext/RichTextEditing.kt`): `setAlignment(content, start, end, align)` + `alignmentAt(...)` with paragraph-range bookkeeping, mirroring `toggleBlock`. - `setAlignment` marks every line the selection touches and leaves untouched paragraphs alone. `START` (the writing-direction default) is stored as *no* alignment — the range is dropped — so an otherwise-plain paragraph stays plaintext-only; `CENTER`/`END` become explicit ranges. - Output is the **same canonical form** `RichTextHtml.fromHtml` returns: one merged range per run of adjacent same-aligned lines, and blank paragraphs anchor no range (the HTML model can't pin `text-align` to an empty `<p>`). A unit test asserts `setAlignment` output is a serialize→parse **fixpoint**, so model / HTML / editor rendering never drift. - `alignmentAt` returns the shared alignment (`START` default for plain paragraphs, `null` when mixed), driving the control's three-state highlight directly. - **Control** (`ui/compose/format/ParagraphAlignmentControl.kt`): three glyph buttons (`⇤` / `↔` / `⇥`), each carrying its accessible action via `onClickLabel` like the toolbar's other buttons. Self-contained and testable in isolation, mirroring `FontSizePicker`. - **Toolbar placement**: appended at the **end** of the toolbar, after the block/link buttons and the `FontSizePicker`. This applies the #73 lesson — the toolbar overflows and scrolls horizontally, and `ComposeScreenTest`'s no-scroll `performClick("•")` needs the bullet/numbered/quote buttons to keep their exact on-screen positions, so no new control may be inserted before them. - **Rendering**: `applyAlignment` routes through the existing `ParagraphStyle(textAlign)` mapping (already present from the foundation), preserving the selection since alignment never changes the text. ## Rendering note In-editor rendering uses the foundation's existing `ParagraphStyle(textAlign)` path (model↔`AnnotatedString` round-trip is unit-tested). The model and its HTML are the source of truth; if on-device `ParagraphStyle` rendering misbehaves in the field, the model/HTML stay correct — nothing is faked. ## Test plan - [x] `:app:assembleDebug` - [x] `:app:testDebugUnitTest` — `RichTextEditingTest`: `setAlignment`/`alignmentAt` across caret, multi-paragraph merge, mid-block split, untouched paragraphs, blank lines, empty document, and mixed selections, plus a serialize→parse round-trip fixpoint; `RichTextEditorTest`: `applyAlignment` (centers + keeps selection, START clears). - [x] `:app:lintDebug` - [x] `:app:ktlintCheck :app:detekt` - [x] `:app:compileDebugAndroidTestKotlin` — new compile-only `ParagraphAlignmentControlTest` drives the control in isolation (glyphs shown; each tap reports its `RichAlign`). Emulator E2E left to CI. Closes #76 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.