- The composer measured its rows from the last layout, which can't see the lines scrolled out of view, so near its height limit it shrank, scrolled, regrew and shrank again on every keystroke. It now also shapes the text at its width, a count that doesn't depend on the scroll. - The draft you're typing no longer shows as a card in the sidebar; it appears once you leave it and hides again when you come back. - A long first line truncates in the draft card instead of widening it.
Showing 2 changed files with 58 additions and 4 deletions.
| @@ -204,6 +204,9 @@ const PADDING_Y: f32 = 8.; | ||
| 204 | 204 | /// How much further right an editor starts its text than a textarea: it pads |
| 205 | 205 | /// 6px instead of 10px, plus a 6px gap where the line numbers would end. |
| 206 | 206 | const EDITOR_INDENT: f32 = 2.; |
| 207 | + /// How far short of its width the editor wraps: its 6px gutter margin and | |
| 208 | + /// 10px right margin. | |
| 209 | + const EDITOR_WRAP_INSET: f32 = 16.; | |
| 207 | 210 | |
| 208 | 211 | /// Renders a [`composer_state`] editor as the plain composer would render its |
| 209 | 212 | /// `Textarea`: borderless, in the UI font at the textarea's size and leading, |
| @@ -236,13 +239,22 @@ impl RenderOnce for MarkdownInput { | ||
| 236 | 239 | let state = self.state.read(cx); |
| 237 | 240 | let line_height = state.line_height().unwrap_or_else(|| rems(1.25).to_pixels(window.rem_size())); |
| 238 | 241 | let rows = content_rows(state).unwrap_or(min).clamp(min, max); |
| 242 | + // The rows from the last layout can't be read while the text is | |
| 243 | + // scrolled; shaping the text gives a count that doesn't depend on | |
| 244 | + // the scroll, so the box no longer shrinks, scrolls, regrows and | |
| 245 | + // shrinks again as you type. | |
| 246 | + let measure = move |state: &EditorState, window: &mut Window| { | |
| 247 | + let shaped = shaped_rows(state, max, window); | |
| 248 | + content_rows(state).max(shaped).unwrap_or(min).clamp(min, max) | |
| 249 | + }; | |
| 250 | + let rows = rows.max(shaped_rows(state, max, window).unwrap_or(0)).clamp(min, max); | |
| 239 | 251 | |
| 240 | 252 | // The rows above come from the last layout. Once this frame has laid |
| 241 | 253 | // the text out, measure again and redraw if the content grew or shrank. |
| 242 | 254 | let entity = self.state.clone(); |
| 243 | - window.on_next_frame(move |_, cx| { | |
| 244 | - let measured = content_rows(entity.read(cx)).map(|now| now.clamp(min, max)); | |
| 245 | - if measured.is_some_and(|now| now != rows) { | |
| 255 | + window.on_next_frame(move |window, cx| { | |
| 256 | + let measured = measure(entity.read(cx), window); | |
| 257 | + if measured != rows { | |
| 246 | 258 | cx.notify(entity.entity_id()); |
| 247 | 259 | } |
| 248 | 260 | }); |
| @@ -304,6 +316,41 @@ fn content_rows(state: &EditorState) -> Option<usize> { | ||
| 304 | 316 | Some(measured + lines - visible.len()) |
| 305 | 317 | } |
| 306 | 318 | |
| 319 | + /// How many rows the editor's text wraps to at its width, shaped the way it | |
| 320 | + /// paints (the UI font at the composer's size), counting no further than | |
| 321 | + /// `limit`. `None` before the editor has a width. | |
| 322 | + fn shaped_rows(state: &EditorState, limit: usize, window: &mut Window) -> Option<usize> { | |
| 323 | + // The editor wraps short of its width by its gutter and right margin. | |
| 324 | + let width = state.input_bounds().size.width - px(EDITOR_WRAP_INSET); | |
| 325 | + if width <= px(0.) { | |
| 326 | + return None; | |
| 327 | + } | |
| 328 | + let text = state.text().to_string(); | |
| 329 | + let font_size = rems(0.875).to_pixels(window.rem_size()); | |
| 330 | + let font = font(metal::SANS); | |
| 331 | + let mut rows = 0; | |
| 332 | + for line in text.split('\n') { | |
| 333 | + if rows > limit { | |
| 334 | + break; | |
| 335 | + } | |
| 336 | + if line.is_empty() { | |
| 337 | + rows += 1; | |
| 338 | + continue; | |
| 339 | + } | |
| 340 | + let run = TextRun { | |
| 341 | + len: line.len(), | |
| 342 | + font: font.clone(), | |
| 343 | + color: black(), | |
| 344 | + background_color: None, | |
| 345 | + underline: None, | |
| 346 | + strikethrough: None, | |
| 347 | + }; | |
| 348 | + let shaped = window.text_system().shape_text(line.to_string().into(), font_size, &[run], Some(width), None).ok()?; | |
| 349 | + rows += shaped.iter().map(|line| line.wrap_boundaries().len() + 1).sum::<usize>(); | |
| 350 | + } | |
| 351 | + Some(rows.max(1)) | |
| 352 | + } | |
| 353 | + | |
| 307 | 354 | fn style_for(flags: u16, p: &Palette) -> HighlightStyle { |
| 308 | 355 | let mut style = HighlightStyle::default(); |
| 309 | 356 | if flags & (flag::HEADING | flag::STRONG) != 0 { |