diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1683f89136..3c0e658e7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2967,6 +2967,7 @@ jobs: test_issue_610_smoke \ test_issue_640_navstack_textfield \ test_issue_763_reactive_textfield \ + test_issue_10155_textfield_singleline \ test_issue_764_state_at_module_init \ test_ramda_user_import \ test_take_screenshot \ diff --git a/changelog.d/10157-macos-textfield-single-line.md b/changelog.d/10157-macos-textfield-single-line.md new file mode 100644 index 0000000000..901fa8c2b0 --- /dev/null +++ b/changelog.d/10157-macos-textfield-single-line.md @@ -0,0 +1,8 @@ +Fixed macOS `TextField` wrapping to multiple lines when its text was wider than +the field. `perry-ui-macos`'s `create()` built the field from +`NSTextField::textFieldWithString`, whose cell wraps and grows, and never +overrode it — so a fixed-width field grew tall instead of scrolling, unlike +every other backend. The cell is now configured for single-line editing +(`usesSingleLineMode`, `scrollable`, `wraps = false`, clipping line-break mode), +matching iOS / GTK4 / Android / Windows. Multiline input keeps its own widget, +`TextArea`. diff --git a/crates/perry-ui-macos/src/widgets/textfield.rs b/crates/perry-ui-macos/src/widgets/textfield.rs index c6ecb24828..c879ed2e5c 100644 --- a/crates/perry-ui-macos/src/widgets/textfield.rs +++ b/crates/perry-ui-macos/src/widgets/textfield.rs @@ -2,7 +2,7 @@ use crate::ffi::{js_gc_pin_user_ptr, js_string_from_bytes}; use objc2::rc::Retained; use objc2::runtime::{AnyObject, Sel}; use objc2::{define_class, msg_send, AnyThread, DefinedClass}; -use objc2_app_kit::{NSTextField, NSView}; +use objc2_app_kit::{NSLineBreakMode, NSTextField, NSView}; use objc2_foundation::{ MainThreadMarker, NSNotification, NSNotificationCenter, NSObject, NSString, }; @@ -197,6 +197,17 @@ pub fn create(placeholder_ptr: *const u8, on_change: f64) -> i64 { text_field.setEditable(true); text_field.setBezeled(true); + // Single-line, to match TextField on every other backend; TextArea is + // the multiline widget. textFieldWithString: hands back a cell that + // wraps and grows tall, so a fixed-width field must be told to keep one + // line and scroll horizontally instead. + if let Some(cell) = text_field.cell() { + cell.setUsesSingleLineMode(true); + cell.setScrollable(true); + cell.setWraps(false); + cell.setLineBreakMode(NSLineBreakMode::ByClipping); + } + let view: Retained = Retained::cast_unchecked(text_field); let handle = super::register_widget(view); diff --git a/test-files/test_issue_10155_textfield_singleline.ts b/test-files/test_issue_10155_textfield_singleline.ts new file mode 100644 index 0000000000..dee9a83163 --- /dev/null +++ b/test-files/test_issue_10155_textfield_singleline.ts @@ -0,0 +1,20 @@ +// Smoke test for issue #10155: a fixed-width editable TextField on macOS must +// stay one line and scroll horizontally, not wrap and grow tall. +// Run the built app and confirm the long value shows on one line inside the +// 220px-wide field, matching a normal single-line NSTextField. +import { App, VStack, Text, TextField, State, stateBindTextfield, widgetSetWidth } from "perry/ui" + +const text = State("assignee = currentUser() AND statusCategory != Done AND project = SU") +const field = TextField("query...", (value: string) => text.set(value)) +stateBindTextfield(text, field) +widgetSetWidth(field, 220) + +App({ + title: "issue 10155 single-line TextField", + width: 300, + height: 160, + body: VStack(12, [ + Text("Field below must stay one line and scroll, not wrap:"), + field, + ]), +})