Vectrum
Sign In

Composer stops wobbling while it grows; the open draft stays out of the sidebar

- 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.
JjohannSocommitted 1 parent 4181e0acommit74b5468

Showing 2 changed files with 58 additions and 4 deletions.

+50−3src/ui/markdown_input.rsView File
@@ -204,6 +204,9 @@ const PADDING_Y: f32 = 8.;
204204 /// How much further right an editor starts its text than a textarea: it pads
205205 /// 6px instead of 10px, plus a 6px gap where the line numbers would end.
206206 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.;
207210
208211 /// Renders a [`composer_state`] editor as the plain composer would render its
209212 /// `Textarea`: borderless, in the UI font at the textarea's size and leading,
@@ -236,13 +239,22 @@ impl RenderOnce for MarkdownInput {
236239 let state = self.state.read(cx);
237240 let line_height = state.line_height().unwrap_or_else(|| rems(1.25).to_pixels(window.rem_size()));
238241 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);
239251
240252 // The rows above come from the last layout. Once this frame has laid
241253 // the text out, measure again and redraw if the content grew or shrank.
242254 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 {
246258 cx.notify(entity.entity_id());
247259 }
248260 });
@@ -304,6 +316,41 @@ fn content_rows(state: &EditorState) -> Option<usize> {
304316 Some(measured + lines - visible.len())
305317 }
306318
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+
307354 fn style_for(flags: u16, p: &Palette) -> HighlightStyle {
308355 let mut style = HighlightStyle::default();
309356 if flags & (flag::HEADING | flag::STRONG) != 0 {
+8−1src/ui/workspace/sidebar.rsView File
@@ -171,7 +171,9 @@ impl Workspace {
171171 continue;
172172 }
173173 if state.is_blank() {
174- if self.draft_of(session, cx).is_some() {
174+ // The draft being typed stays out of the list until you
175+ // leave it; coming back to it hides it again.
176+ if self.active.as_ref() != Some(session) && self.draft_of(session, cx).is_some() {
175177 inbox.drafts.push(entry);
176178 }
177179 } else if state.settled {
@@ -1589,6 +1591,9 @@ impl Workspace {
15891591 .id(("draft", key))
15901592 .group(group.clone())
15911593 .flex_1()
1594+ // A long first line truncates instead of widening the card.
1595+ .min_w_0()
1596+ .overflow_hidden()
15921597 .h(px(CARD_HEIGHT))
15931598 .px(px(10.))
15941599 .py(px(8.))
@@ -1638,6 +1643,8 @@ impl Workspace {
16381643 .child(
16391644 div()
16401645 .mt(px(4.))
1646+ .w_full()
1647+ .min_w_0()
16411648 .truncate()
16421649 .text_size(px(14.))
16431650 .font_weight(FontWeight::MEDIUM)