Skip to content

feat(text): markdown formatting with floating format bar - #175

Draft
devin-ai-integration[bot] wants to merge 8 commits into
mainfrom
devin/1789961672-text-markdown-format-bar
Draft

devin-ai-integration[bot] wants to merge 8 commits into
mainfrom
devin/1789961672-text-markdown-format-bar

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Text widgets now understand a small Markdown subset and get a floating format bar while editing, so people who don't know the syntax can still bold, bullet, number, and head their notes. Always on, no setting. PlacedText.text stays a plain String (the source with markers), so nothing changes for Hive, migrations, or .ica round-trips; old strategies render exactly as before unless they happen to contain markers.

Supported: **bold**, *italic*/_italic_, - /* / bullets, 1. /1) numbered, #### headings. Unclosed markers stay literal. No Markdown dependency; the parser is ~200 lines in text_markup.dart.

How it fits together

text_markup.dart                 parseMarkup(String) -> List<MarkupLine>
                                 MarkupEditing.toggleInline / toggleLineKind / continueList
markup_text_editing_controller   TextEditingController.buildTextSpan: dims markers, styles bold/italic/headings in the raw source
formatted_text_view.dart         non-editing render: markers hidden, • glyphs, computed list numbers
text_format_bar.dart             B / I / bullets / numbered / H, active glyph in violet
text_widget.dart                 editing ? TextField(controller) : FormattedTextView; bar shown through OverlayPortal
  • Not editing → FormattedTextView. Tap → TextField(decoration: null) with MarkupTextEditingController, both inside the same 12px vertical padding, so the card is the same height in either state (hint is drawn by hand for the empty case); focus loss commits the draft and returns to the formatted view. Same swap for drag feedback, screenshots, and page transitions, so every non-editing path renders formatted.
  • The bar lives in an OverlayPortal (not scaled by map zoom), centred under the widget's transformed bounds, flips above when there's no room below, clamped to the viewport. Its taps share a TapRegion group with the field so clicking a button doesn't end editing.
  • Line-kind toggles apply to the lines touched by the selection only; numbered toggles renumber the run. Enter on a list item continues it (ListContinuationFormatter), Enter on an empty item exits the list. Ctrl/Cmd+B and +I toggle inline marks; with a collapsed caret they wrap the word under the caret, or insert **** and park the caret inside.
  • Geometry is untouched: the widget's measured box is still the frame (_TextBoxFrame), and the overlay is outside it, so resize, drag, and defense-side mirroring behave as before.

EditorToolbarButton gained an active flag: the glyph turns violet (DESIGN.md: violet on an icon means checked), no fill.

One test removed: canonical_coordinates_test asserted the live card height equals PlacedMediaGeometry.legacyTextFootprintInWorld. That formula only feeds the one-time canonical-coordinates migration and models the old TextField card; the runtime mirrors from live measurement, and the new card (text + 12px padding, no Material chrome) is a different height at the same font, so the two can't stay equal by design. Shout if you'd rather I pin the card to the old footprint instead.

Screenshots (Linux run)

Editing, bar below the widget, markers dimmed, active kind in violet:

editing

Same widget not editing (same card height):

rendered-same-size

Rendered after focus loss, after a resize:

rendered

Heading + numbered list + inline bold rendered:

rendered-list

Defense side (mirrored), formatting intact:

defense

Exported screenshot (camera button) crop:

export

Video walkthrough is in the first comment.

Tests

test/text_markup_test.dart, test/formatted_text_view_test.dart new; test/text_widget_resilience_test.dart extended (enter editing, draft, commit on blur). fvm flutter analyze clean for touched files; targeted suite +42 passed.

Link to Devin session: https://app.devin.ai/sessions/dc7ec45e6e12464494017e5ba546e542
Open in Devin Desktop: https://app.devin.ai/desktop/session/dc7ec45e6e12464494017e5ba546e542?variant=devin
Requested by: @SunkenInTime

SunkenInTime and others added 3 commits September 21, 2026 03:41
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: c9e0227c-133d-4d71-ba63-3306ac0b02d1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Video walkthrough, recorded in the Linux desktop build: heading, numbering only the selected lines, bullet continuation and exit, Ctrl+B, italic from the bar, click-outside rendering, then resize, drag, Defense mirroring and PNG export.

Markdown walkthrough

Editing Rendered
Editing Rendered
Defense mirroring, exported PNG

Defense

Export

Not covered by the video: persistence across reopen and .ica round-trip (the model is unchanged, so both are exercised by existing tests only).

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@greptile-apps

greptile-apps Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

Do not merge until the floating format bar is kept within the overlay bounds.

Findings

  1. P1 Top-edge toolbar is clipped

Summary

This change adds Markdown-style formatting for text widgets, including formatted display rendering, source editing, keyboard shortcuts, list continuation, and a floating formatting bar.

Drafts are retained when an edited widget is removed, unmatched markers remain visible as literal text, and screenshot and drag-feedback paths use non-interactive formatted rendering. The floating formatting bar still has a top-edge placement failure: when a tall text widget near the top cannot fit the bar below it, the fallback places the controls outside the visible overlay.

Reviews (1) · Last reviewed commit: "fix(text): scope line-kind toggles to th..."

Comment on lines +279 to +282
final below = childRect.bottom + 8;
final top = below + TextFormatBar.height + 8 <= overlaySize.height
? below
: childRect.top - TextFormatBar.height - 8;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Top-edge toolbar is clipped

When the bar cannot fit below a tall text widget near the top of the overlay, the fallback puts it above the widget without bounding the result. A frame at top = 10 calculates 10 - 36 - 8 = -34, leaving nearly all of the 36px toolbar outside the viewport and its formatting controls inaccessible. Clamp the fallback to a visible overlay inset, or use a bounded placement when neither preferred side has enough room.

T-Rex Ran code and verified through T-Rex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1842972: top is now clamped to [8, overlayHeight - barHeight - 8], same as left.

@greptile-apps

greptile-apps Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff

These findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.

  • P1 Top-edge tall text frame clips the floating formatting toolbar

    • Bug
      • When a tall editable text frame starts near the top of the overlay and cannot fit the format bar below, the fallback branch computes a negative Positioned.top. For a frame top of 10px, it is 10 - 36 - 8 = -34; the entry transform leaves the toolbar around -30px, so almost the entire 36px bar and its controls are outside the viewport.
    • Cause
      • text_widget.dart:279-282 chooses above-frame placement when below-frame placement does not fit, but does not constrain the resulting top position to the overlay bounds.
    • Fix
      • Clamp the above-frame top to a visible overlay inset and, when neither side has sufficient room, use a bounded fallback placement that keeps the full toolbar visible.

SunkenInTime and others added 4 commits September 21, 2026 04:18
…heck

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… for active

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant