โ Final Challenge
โบ The Editor Core
75 ptsTime to assemble the machine. EditorCore is the 100%-portable heart of
the editor: document, layout, selection, editing, undo, damage โ driven
entirely by synthetic events and queried entirely through values. In the
real application, the platform layer you studied in lessons 1โ4 is a thin
shell around exactly this class: the X11 (or Win32, or Cocoa) event
switch translates native events into these method calls, and the paint
handler asks take_damage() what to repaint. Here, the test harness
plays the platform: it clicks, drags, types, and asserts.
Use the pieces you've built: the piece table is the intended document
store (a std::string will pass the tests โ behavior is all that's
graded โ but you'd be skipping the point), the line index and wrap
algorithm drive layout, the anchor/focus model drives selection, and the
lesson-14 stack drives undo. The starter provides, fully implemented: the
UTF-8 boundary functions (lesson 6), word_at and classify (lesson
11), and Rect with union_of (lesson 2). Everything else is yours.
The font is a fake fixed-width face: every codepoint advances 8
pixels (kAdvance), and rows are line_height pixels tall. This is
the "stub metrics" trick โ with geometry this predictable, the tests can
compute expected pixel math by hand, while your code stays structured
exactly as if a real FontMetrics were plugged in.
The contract, in five parts. It is deliberately exhaustive โ every rule below is exercised by at least one test.
Layout. The document is logical lines split on '\n' (lesson 8
conventions: offset 0 always starts a line; a trailing newline yields a
final empty line). Each line wraps greedily at wrap_width pixels per
lesson 9's exact rules, generalized to codepoints: iterate UTF-8
boundaries, 8px per codepoint, break opportunities after ' ', strictly-
greater overflow, hard-break inside long words, always make progress; an
empty line is one empty row. The document's rows are all lines' rows
concatenated, top to bottom; row_count() reports the total. Row i
covers pixels y โ [i*line_height, (i+1)*line_height). The displaying
row of an offset (needed for damage) is the last row whose begin <= offset โ i.e. wrap boundaries display downstream, no affinity bit in
this final.
Hit testing. click/double_click/drag take pixel coordinates:
clamp y to a row (negative โ first, beyond โ last), then resolve x
within the row by the half-advance rule with ties right (x < left + 4
puts the caret before the codepoint); x < 0 โ row start, past the last
glyph โ row end.
Selection & movement. Anchor/focus (lesson 11); caret() returns the
focus; sel_begin()/sel_end() the normalized ends. click collapses
both to the hit; drag moves only the focus; double_click selects
word_at(hit) (anchor at its begin, focus at its end). left/right
with shift move only the focus by one UTF-8 boundary (clamped at the
document ends); without shift, a non-empty selection collapses to its
begin/end (no extra movement), and an empty one moves one boundary.
home/end_key go to the start/end of the logical line containing the
focus (end = before the '\n'); with shift they move the focus only,
extending or shrinking the selection. Without shift they collapse both
anchor and focus to that line boundary outright โ unlike plain
left/right, which spend their first press collapsing an existing
selection to its nearer edge, home/end_key always jump all the way to
the line start/end even if a selection was active.
Editing & undo. insert_text(s) (empty s: total no-op) replaces
the selection (possibly empty) with s; caret lands after s,
collapsed. backspace deletes the selection, or one codepoint before the
caret (no-op at offset 0); key_delete deletes the selection, or one
codepoint after (no-op at the end). Every text change records a lesson-14
EditOp {pos, removed, inserted}; recording clears redo. Consecutive
insert_text calls coalesce under exactly the lesson-14 rule (both pure
insertions, adjacent, nothing else in between โ any other event,
including clicks and arrows, breaks the run). undo() applies the
inverse of the top op and collapses the caret to op.pos + op.removed.size(); redo() re-applies and lands at op.pos + op.inserted.size(); both are silent no-ops when their stack is empty.
Damage. take_damage() returns the bounding box of everything that
changed since the last call (an empty Rect if nothing did), then
resets. Damage accrues as full-width bands of rows, unioned together:
- Selection change (any event that alters the anchor/focus pair
without changing text): the band from the lowest to the highest of
the displaying rows of {old begin, old end, new begin, new end},
inclusive, i.e.
yfrommin_row * line_heightthrough `(max_row +-
- line_height`. Events that leave anchor and focus unchanged add no damage.
-
- Text change (insert, backspace, delete, undo, redo): re-layout first,
then compute damage against the new text โ let
Lbe the logical line containing the edit position as the document now reads (inserting or deleting a'\n'changes where line boundaries fall, soLmust be found post-edit, not looked up in the old layout), andfirstthe displaying row ofL's first offset. The band runs fromfirst * line_heighttomax(row_count_before, row_count_after) * line_height. (Everything from the edited line to the bottom of the taller layout โ reflow can move every row below the edit.)
Log in to submit a solution and earn points.