Skip to content

feat: Merge box-drawing lines into junctions where they meet - #94

Merged
FXschwartz merged 38 commits into
Norbert515:mainfrom
FXschwartz:5424-t-junction-blending
Aug 10, 2026
Merged

feat: Merge box-drawing lines into junctions where they meet#94
FXschwartz merged 38 commits into
Norbert515:mainfrom
FXschwartz:5424-t-junction-blending

Conversation

@FXschwartz

Copy link
Copy Markdown
Collaborator

Borders and dividers previously painted their cells independently, so a divider ending at a border stopped one cell short or overwrote it, the UI showed gaps where ├ ┤ ┬ ┴ ┼ junctions belong.

What this does

Adds arm-based box-character merging (the approach ratatui/tview use): every char in U+2500–257F maps to four arms with weights, drawing one line char onto another ORs the arms and emits the junction glyph.

  • mergeBoxCharacters() + isMergeableBoxCharacter() in lib/src/utils/box_line_merging.dart, exported from nocterm.dart
  • TerminalCanvas.drawText(..., blendBoxLines:), the internal mechanism the components paint with, defaults to false so ordinary text never merges (a inside a log line still overwrites its cell)
  • Divider/VerticalDivider: always merge; negative indent/endIndent reach into a border row/column and cap with half-arms ( + )
  • BoxBorder: corner cells always merge (overlaid panels tee into the border underneath), edges/titles/backgrounds occlude

Behavior change

Blending is always on for Divider, VerticalDivider, and BoxBorder, there is no per-component flag. Existing apps change appearance wherever lines overlap:

  • Dividers crossing borders/each other now form junctions instead of overwriting.
  • An overlaid panel's four corner cells merge with box characters beneath them. Content under the panel is still occluded by its edges, title, and background, but a corner landing exactly on a box char in the underlay (a inside a log line) renders as a junction.
  • Bordered boxes with a background color no longer paint that background over their border ring cells (the border pass paints the ring itself).

Added Example

dart example/box_line_blending_demo.dart - side-by-side indent: 0 vs indent: -1, crossings, mixed-weight tees, and an overlaid panel.

@FXschwartz FXschwartz self-assigned this Jul 10, 2026
Copilot AI review requested due to automatic review settings July 10, 2026 14:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR improves terminal UI rendering by introducing box-drawing “line blending” so overlapping borders/dividers merge into the correct Unicode junction glyphs (e.g. ├ ┤ ┬ ┴ ┼) instead of leaving gaps or overwriting each other.

Changes:

  • Added arm/weight-based merging for box-drawing characters via mergeBoxCharacters() / isMergeableBoxCharacter().
  • Extended TerminalCanvas.drawText() with blendBoxLines to optionally merge box characters while drawing.
  • Enabled blending behavior for Divider/VerticalDivider (including half-arm end caps for negative indents) and for BoxBorder corners; added tests + a demo example.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/utils/box_line_merging_test.dart Unit tests for low-level box-character merging behavior (tees/crosses/weights/mergeability).
test/rendering/border_blend_test.dart Rendering tests validating overlay border corners tee into underlying borders and background doesn’t erase underlying border ring cells.
test/components/divider_blend_test.dart Rendering tests ensuring dividers tee into borders with negative indents and form crosses when intersecting.
lib/src/utils/box_line_merging.dart Implements the arm/weight model and merge logic for Unicode box-drawing characters.
lib/src/framework/terminal_canvas.dart Adds blendBoxLines option to drawText() to merge box-drawing characters with existing buffer content.
lib/src/components/divider.dart Makes dividers blend by default (except ascii), adds half-arm caps for negative indents, and updates component docs.
lib/src/components/decorated_box.dart Adjusts background fill to avoid erasing border ring cells; enables blending for border corner cells.
lib/nocterm.dart Exports the new box line merging utilities.
example/box_line_blending_demo.dart Adds an example showcasing divider/border blending and negative-indent behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/src/components/decorated_box.dart
Comment thread lib/src/components/decorated_box.dart
Comment thread lib/src/components/decorated_box.dart
Comment thread lib/src/components/decorated_box.dart
Comment thread lib/src/components/decorated_box.dart
Comment thread lib/src/components/decorated_box.dart
Comment thread lib/src/components/divider.dart Outdated
Comment thread lib/src/components/divider.dart Outdated
@nielsenko

Copy link
Copy Markdown
Collaborator

@FXschwartz Really nice feature.

I have suggested some changes here FXschwartz#1 that I think should be included before merging.

image

@nielsenko nielsenko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same-weight pairs and cross-weight pairs with disjoint arms are order
independent; shared arms let the newly drawn weight win.
A double divider reaching into a border merges as a cross (╪ ╫ ╬) instead
of a tee (╞ ╡, ╥ ╨, ╠ ╣). The light-divider-into-double-border test
passes, pinning the gap to double caps.
Unicode has half lines at light and heavy weight only, so a lone double
arm has no glyph and the end cell fell back to ═/║, merging as a cross.
Ends are now expressed as arms - mergeArmsIntoCharacter plus a blendArms
override on drawText - so ╞ ╡ ╥ ╨ ╠ ╣ form.
Covers the arm combination Unicode cannot name, and ends landing on a
cell with nothing to merge into.
nielsenko and others added 17 commits August 7, 2026 12:35
A zero-extent rule reaching into a border painted only the border cell,
leaving a tee pointing at a line that does not exist.
Stamping the half-arm regardless overwrote a title character, and punched
holes in a border the arm had no glyph to join.
isMergeableBoxCharacter never had a caller; its tests went with it.
Directory.current is process-wide, not per-isolate, so tests that chdir
to their own temp directory raced under isolate-per-test.
getProjectDirectory now takes the directory to walk up from.
The path is keyed by process id - fine for one server per app, not for a
test file that starts one per test under isolate-per-test, where they
overwrote, deleted and read each other's ports.
Dividers already had heavy lines; borders did not, so ┠ ┨ ┰ ┸ had no way
to occur.
Adds junctions the light-only version never showed: ├ from a dashed rule,
╥ ╨ from a double divider, ╢ where the two meet, and ┠ ┨ ┯ ┷ against the
bold border.
The panel moves into the pane so it appears in both columns, and now
carries a background: it hides what it covers while its corners still tee
into the border. Title tests move to 80x40, where the panes have room.
Three dividers reach into one fixed-length title, so the columns are
exact at any width rather than an 80-column coincidence.
drawJunction takes a position and the arms, nothing else. Leaving the
cell alone when there is nothing to join falls out of it rather than
being a branch inside drawText.

@nielsenko nielsenko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@FXschwartz
FXschwartz merged commit 85996a4 into Norbert515:main Aug 10, 2026
2 of 3 checks passed
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.

Use T-junction box-drawing characters to remove gaps between borders and dividers in the serverpod start TUI

3 participants