input: Fix wrong font being used in Input. (#1706)

Closes #1665

## Description

When `Input::new().font_family("font")` is used, the font hasn't been
applied yet in `Input::render`, therefore the `text_wrapper` will
calculate using the wrong font. This is why it's working when the font
is added to the parent element.

The fix moves the state update to `element` prepaint, where
`window.text_style()` has the correct styles. The only downside is that
there are now one additional state update.

## Screenshot

| Before                       | After                       |
| ---------------------------- | --------------------------- |
| <img width="1125" height="246" alt="Screenshot From 2025-11-30
20-03-11"
src="https://github.com/user-attachments/assets/1fa798a7-0b9c-4386-8940-ad5f6734b8a0"
/> | <img width="1125" height="246" alt="Screenshot From 2025-11-30
20-00-23"
src="https://github.com/user-attachments/assets/e970e7bc-1bfa-4a55-a342-3381d1684526"
/> |

## How to Test

Use some wide font, like `IBM Plex Mono`. Apply it to
`Input::new(&some_state).font_family("IBM Plex Mono")` where the input
state has `soft_wrap`. Here is a diff for `textarea_story.rs`.

```diff
diff --git a/crates/story/src/textarea_story.rs b/crates/story/src/textarea_story.rs
index f2c63d44..bd5cbb31 100644
--- a/crates/story/src/textarea_story.rs
+++ b/crates/story/src/textarea_story.rs
@@ -77,9 +77,9 @@ impl TextareaStory {
 
         let textarea_no_wrap = cx.new(|cx| {
             InputState::new(window, cx)
-                .multi_line(true)
                 .rows(6)
-                .soft_wrap(false)
+                .multi_line(true)
+                .soft_wrap(true)
                 .default_value("This is a very long line of text to test if the horizontal scrolling function is working properly, and it should not wrap automatically but display a horizontal scrollbar.\nThe second line is also very long text, used to test the horizontal scrolling effect under multiple lines, and you can input more content to test.\nThe third line: Here you can input other long text content that requires horizontal scrolling.\n")
         });
 
@@ -195,7 +185,7 @@ impl Render for TextareaStory {
             .child(
                 section("Auto Grow")
                     .max_w_md()
-                    .child(Input::new(&self.textarea_auto_grow)),
+                    .child(Input::new(&self.textarea_auto_grow).font_family("IBM Plex Mono")),
             )
             .child(
                 section("Auto Grow with No Wrap")
```


## Checklist

- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] Reviewed the changes in this PR and confirmed AI generated code
(If any) is accurate.
- [x] Passed `cargo run` for story tests related to the changes.
- [ ] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
This commit is contained in:
Andreas Johansson 2025-12-01 10:05:34 +01:00 committed by GitHub
parent 0f655314c9
commit 0126f1b036
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 28 additions and 31 deletions

View file

@ -85,8 +85,9 @@ impl InputStory {
fraction: Some(3),
})
});
let custom_input =
cx.new(|cx| InputState::new(window, cx).placeholder("here is a custom input"));
let custom_input = cx.new(|cx| {
InputState::new(window, cx).placeholder("Custom Input use monospace, 0123456789.")
});
let code_input = cx.new(|cx| {
InputState::new(window, cx)
@ -247,11 +248,12 @@ impl Render for InputStory {
))),
)
.child(
section("Appearance false").max_w_md().child(
section("Custom Appearance").max_w_md().child(
div()
.border_b_2()
.px_6()
.py_3()
.font_family(cx.theme().mono_font_family.clone())
.border_color(cx.theme().border)
.bg(cx.theme().secondary)
.text_color(cx.theme().secondary_foreground)

View file

@ -818,6 +818,15 @@ impl Element for TextElement {
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
let style = window.text_style();
let font = style.font();
let text_size = style.font_size.to_pixels(window.rem_size());
self.state.update(cx, |state, cx| {
state.text_wrapper.set_font(font, text_size, cx);
state.text_wrapper.prepare_if_need(&state.text, cx);
});
let state = self.state.read(cx);
let line_height = window.line_height();
@ -840,8 +849,7 @@ impl Element for TextElement {
let text = state.text.clone();
let is_empty = text.len() == 0;
let placeholder = self.placeholder.clone();
let style = window.text_style();
let font_size = style.font_size.to_pixels(window.rem_size());
let mut bounds = bounds;
let (display_text, text_color) = if is_empty {
@ -862,7 +870,7 @@ impl Element for TextElement {
// Calculate the width of the line numbers
let (line_number_width, line_number_len) =
Self::layout_line_numbers(&state, &text, font_size, &text_style, window);
Self::layout_line_numbers(&state, &text, text_size, &text_style, window);
let wrap_width = if multi_line && state.soft_wrap {
Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
@ -956,7 +964,7 @@ impl Element for TextElement {
&state,
&display_text,
&last_layout,
font_size,
text_size,
&runs,
&document_colors,
window,
@ -972,7 +980,7 @@ impl Element for TextElement {
.text_system()
.shape_line(
longest_line.clone(),
font_size,
text_size,
&[TextRun {
len: longest_line.len(),
font: style.font(),
@ -1084,7 +1092,7 @@ impl Element for TextElement {
sub_lines.push(
window
.text_system()
.shape_line(line_no, font_size, &runs, None),
.shape_line(line_no, text_size, &runs, None),
);
for _ in 0..line.wrapped_lines.len().saturating_sub(1) {
sub_lines.push(ShapedLine::default());

View file

@ -2,7 +2,7 @@ use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity, InteractiveElement as _,
IntoElement, IsZero, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement,
Styled, Window, div, px, relative, rems,
Styled, Window, div, px, relative,
};
use crate::button::{Button, ButtonVariants as _};
@ -241,16 +241,8 @@ impl Styled for Input {
impl RenderOnce for Input {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
const LINE_HEIGHT: Rems = Rems(1.25);
let font = window.text_style().font();
let font_size = match self.size {
Size::Large => rems(1.),
_ => rems(0.875),
}
.to_pixels(window.rem_size());
self.state.update(cx, |state, cx| {
state.text_wrapper.set_font(font, font_size, cx);
state.text_wrapper.prepare_if_need(&state.text, cx);
self.state.update(cx, |state, _| {
state.disabled = self.disabled;
state.size = self.size;
});
@ -365,8 +357,8 @@ impl RenderOnce for Input {
.input_px(self.size)
.input_py(self.size)
.input_h(self.size)
.input_text_size(self.size)
.cursor_text()
.text_size(font_size)
.items_center()
.when(state.mode.is_multi_line(), |this| {
this.h_auto()

View file

@ -368,11 +368,7 @@ impl InputState {
Self {
focus_handle: focus_handle.clone(),
text: "".into(),
text_wrapper: TextWrapper::new(
text_style.font(),
text_style.font_size.to_pixels(window.rem_size()),
None,
),
text_wrapper: TextWrapper::new(text_style.font(), window.rem_size(), None),
blink_cursor,
history,
selected_range: Selection::default(),

View file

@ -1,6 +1,6 @@
use std::ops::Range;
use gpui::{point, px, size, App, Font, LineFragment, Pixels, Point, ShapedLine, Size, Window};
use gpui::{App, Font, LineFragment, Pixels, Point, ShapedLine, Size, Window, point, px, size};
use ropey::Rope;
use smallvec::SmallVec;
@ -500,7 +500,7 @@ impl LineLayout {
#[cfg(test)]
mod tests {
use super::*;
use gpui::{px, Boundary, FontFeatures, FontStyle, FontWeight};
use gpui::{Boundary, FontFeatures, FontStyle, FontWeight, px};
#[test]
fn test_update() {

View file

@ -437,9 +437,9 @@ impl<T: Styled> StyleSized<T> for T {
match size {
Size::XSmall => self.text_xs(),
Size::Small => self.text_sm(),
Size::Medium => self.text_base(),
Size::Large => self.text_lg(),
Size::Size(size) => self.text_size(size),
Size::Medium => self.text_sm(),
Size::Large => self.text_base(),
Size::Size(size) => self.text_size(size * 0.875),
}
}
@ -477,7 +477,6 @@ impl<T: Styled> StyleSized<T> for T {
Size::XSmall => self.h_5(),
_ => self.h_6(),
}
.input_text_size(size)
}
#[inline]