)]}'
{
  "commit": "9c931498d505ba8d3ce0c1f033333afa7d445553",
  "tree": "3b4fecede391d28c543b65ca7652ecde2022171c",
  "parents": [
    "ab50e0d1e44b2744d4f8b34dd89f3fabd7c2bdcd"
  ],
  "author": {
    "name": "Rusino",
    "email": "Rusino@users.noreply.github.com",
    "time": "Tue Sep 29 18:21:49 2026 +0000"
  },
  "committer": {
    "name": "GitHub",
    "email": "noreply@github.com",
    "time": "Tue Sep 29 18:21:49 2026 +0000"
  },
  "message": "[WebParagraph] Fixing edge cases for wrapping text (with newlines) (#189858)\n\n### Summary\n\nFixes newline (`\\n`) handling and caret/selection geometry in\n`WebParagraph` to match `SkParagraph` behavior, resolving misplaced\ncursor issues in multiline text fields on the web.\n\nFixes https://github.com/flutter/flutter/issues/188320\n\n---\n\n### Background\n\nIn Flutter text editing (`RenderEditable`), caret positioning, line\nnavigation, and selection rely directly on `Paragraph` query APIs\n(`getPositionForOffset`, `getBoxesForRange`, `getLineBoundary`,\n`getLineNumberAt`, and `getGlyphInfoAt`).\n\nPreviously, `WebParagraph` diverged from `SkParagraph` in several key\nnewline edge cases:\n1. **Physical vs. Logical Width:** Browser Canvas/DOM text measurement\ntreats `\\n` as whitespace with positive advance width. In `SkParagraph`,\nnewlines occupy zero physical width while retaining their logical text\nranges.\n2. **Trailing Newlines:** Text ending in a newline (e.g. `\"Hello\\n\"`)\nmust generate an additional empty line where the cursor resides after\npressing Enter.\n3. **Query Parity:** Hit-testing and boundary queries on lines ending\nwith or consisting entirely of newlines returned incorrect offsets or\ncollapsed bounds.\n\n---\n\n### Changes\n\n- **Explicit Hard Line Break Tracking (`wrapper.dart`):**\n- Separated hard line breaks (`\\n`) from collapsible whitespace in\n`_LineBuilder`.\n- Added support for generating the trailing empty line when text\nterminates with a hard line break.\n\n- **Logical vs. Physical Text Ranges (`layout.dart`,\n`paragraph.dart`):**\n- Introduced `physicalTextRange` on `TextBlock` to exclude `\\n` from\nphysical geometry calculations (bounds, selection rects, line widths)\nwhile preserving `textRange` for logical text operations.\n- Adjusted `getGlyphInfoAt` to report zero layout width for `\\n` and\nsynthesize end-of-line glyph bounds for newlines outside visual blocks.\n- Aligned `getPositionForOffset` and `getLineBoundary` with\n`SkParagraph` hit-testing rules, correctly handling empty trailing lines\nand cluster boundary affinity.\n- Implemented `SkParagraph`-matching binary search in `getLineNumberAt`.\n\n---\n\n### Testing\n\n- Added `test/webparagraph/paragraph_edge_cases_test.dart` (newline\nwrapping and empty line creation).\n- Added `test/webparagraph/paragraph_selection_caret_test.dart` (caret\nhit-testing, affinities, and selection bounding boxes across multiline\ntext).\n- Added `test/webparagraph/paragraph_line_metrics_test.dart` (line\ngeometry and width validation).\n- Updated existing `webparagraph` unit tests to ensure 1:1 parity with\n`SkParagraph`.\n\n---------\n\nCo-authored-by: Mouad Debbar \u003cmouad.debbar@gmail.com\u003e\nCo-authored-by: Jackson Gardner \u003cjacksongardner@google.com\u003e",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "37671b65fc7e9a033ef5a1d3e0f290c04f69493b",
      "old_mode": 33188,
      "old_path": "engine/src/flutter/lib/web_ui/lib/src/engine/web_paragraph/layout.dart",
      "new_id": "cc1b0dce75b734d0fb70871a799fd089f71ae7b1",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/lib/src/engine/web_paragraph/layout.dart"
    },
    {
      "type": "modify",
      "old_id": "96c7b7427bc8ddf060a97b074edbea02feba132d",
      "old_mode": 33188,
      "old_path": "engine/src/flutter/lib/web_ui/lib/src/engine/web_paragraph/paragraph.dart",
      "new_id": "fa437e8c4c8bab597b777db63247f92f9775fda8",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/lib/src/engine/web_paragraph/paragraph.dart"
    },
    {
      "type": "modify",
      "old_id": "9a339e48b769f234966b0b58178c200cd5f97676",
      "old_mode": 33188,
      "old_path": "engine/src/flutter/lib/web_ui/lib/src/engine/web_paragraph/wrapper.dart",
      "new_id": "ded0668c52062e220d9e1d9a16dfb6424a188464",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/lib/src/engine/web_paragraph/wrapper.dart"
    },
    {
      "type": "modify",
      "old_id": "6a952a999deadeabd8fba3ba99e3c9c2cc868459",
      "old_mode": 33188,
      "old_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_builder_test.dart",
      "new_id": "76fb8697ed2b0ad8fbca99d7a11ebda8ff3a3b35",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_builder_test.dart"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "fd40bb95bb5eba858eb05e9041d171cf21a90d5e",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_edge_cases_test.dart"
    },
    {
      "type": "modify",
      "old_id": "78b4c0ffde6abba26d04534e470ef25d75abfb2d",
      "old_mode": 33188,
      "old_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_get_boxes_test.dart",
      "new_id": "c6f0c6fac5b96bc376e6a55ef3e15b382eafe38d",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_get_boxes_test.dart"
    },
    {
      "type": "modify",
      "old_id": "8d6415706bb06898c0f137c06997eda25344ad76",
      "old_mode": 33188,
      "old_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_get_position_test.dart",
      "new_id": "31fae79ed9568e558993c3258ff2b7acb124dc83",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_get_position_test.dart"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "f7daf9256ea7ad1079cee892b84327d27b33bb36",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_line_metrics_test.dart"
    },
    {
      "type": "modify",
      "old_id": "382f37a57a3198651321bee9b3d6e5273922ba3c",
      "old_mode": 33188,
      "old_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_queries_test.dart",
      "new_id": "ca8b8d39aebf2b8706d5657a108bd739da44adf7",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_queries_test.dart"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "b09eab8534b6ab94c032f348feb4734cf351479f",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_selection_caret_test.dart"
    },
    {
      "type": "modify",
      "old_id": "fe048725b6f7394d6c1d47f616bc8a8d5e0cf26a",
      "old_mode": 33188,
      "old_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_wrapper_test.dart",
      "new_id": "0d90af304ea0ea5dec89261758414d3aa098eb7e",
      "new_mode": 33188,
      "new_path": "engine/src/flutter/lib/web_ui/test/webparagraph/paragraph_wrapper_test.dart"
    }
  ]
}
