โ˜… Final Challenge

โ€บ The Editor Core

75 pts

Time 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. y from min_row * line_height through `(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 L be the logical line containing the edit position as the document now reads (inserting or deleting a '\n' changes where line boundaries fall, so L must be found post-edit, not looked up in the old layout), and first the displaying row of L's first offset. The band runs from first * line_height to max(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.