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 formRichTextHtml.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).
## 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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Adds a three-state paragraph-alignment control (start / center / end) to the compose formatting toolbar.
richtext/RichTextEditing.kt):setAlignment(content, start, end, align)+alignmentAt(...)with paragraph-range bookkeeping, mirroringtoggleBlock.setAlignmentmarks 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/ENDbecome explicit ranges.RichTextHtml.fromHtmlreturns: one merged range per run of adjacent same-aligned lines, and blank paragraphs anchor no range (the HTML model can't pintext-alignto an empty<p>). A unit test assertssetAlignmentoutput is a serialize→parse fixpoint, so model / HTML / editor rendering never drift.alignmentAtreturns the shared alignment (STARTdefault for plain paragraphs,nullwhen mixed), driving the control's three-state highlight directly.ui/compose/format/ParagraphAlignmentControl.kt): three glyph buttons (⇤/↔/⇥), each carrying its accessible action viaonClickLabellike the toolbar's other buttons. Self-contained and testable in isolation, mirroringFontSizePicker.FontSizePicker. This applies the #73 lesson — the toolbar overflows and scrolls horizontally, andComposeScreenTest's no-scrollperformClick("•")needs the bullet/numbered/quote buttons to keep their exact on-screen positions, so no new control may be inserted before them.applyAlignmentroutes through the existingParagraphStyle(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↔AnnotatedStringround-trip is unit-tested). The model and its HTML are the source of truth; if on-deviceParagraphStylerendering misbehaves in the field, the model/HTML stay correct — nothing is faked.Test plan
:app:assembleDebug:app:testDebugUnitTest—RichTextEditingTest:setAlignment/alignmentAtacross 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-onlyParagraphAlignmentControlTestdrives the control in isolation (glyphs shown; each tap reports itsRichAlign).Emulator E2E left to CI.
Closes #76
🤖 Generated with Claude Code