input: Support content type for Input. (#2495)

This commit is contained in:
Jason Lee
2026-07-06 11:55:03 +00:00
committed by GitHub
parent f0abdd9f85
commit a183e4622b
10 changed files with 774 additions and 15 deletions
+185 -4
View File
@@ -1,10 +1,10 @@
use gpui::{
App, AppContext as _, ClickEvent, Context, Entity, InteractiveElement, IntoElement,
ParentElement as _, Render, Styled, Subscription, Window, div,
ParentElement as _, Render, Role, Styled, Subscription, Window, div,
};
use crate::section;
use gpui_component::{button::*, input::*, *};
use gpui_component::{button::*, input::*, label::Label, *};
const CODE_EXAMPLE: &str = r#"{"single_line":"code editor"}"#;
@@ -32,10 +32,18 @@ pub struct InputStory {
custom_menu_input: Entity<InputState>,
code_input: Entity<InputState>,
color_input: Entity<InputState>,
content_type_inputs: Vec<ContentTypeInput>,
_subscriptions: Vec<Subscription>,
}
struct ContentTypeInput {
label: &'static str,
content_type: InputContentType,
input: Entity<InputState>,
mask_toggle: bool,
}
impl super::Story for InputStory {
fn title() -> &'static str {
"Input"
@@ -139,6 +147,117 @@ impl InputStory {
.default_value("Right Aligned Text")
});
let content_type_inputs = vec![
Self::new_content_type_input(
window,
cx,
"Name",
InputContentType::Name,
"Full name",
"Jane Doe",
false,
),
Self::new_content_type_input(
window,
cx,
"Username",
InputContentType::Username,
"Username",
"jane.doe",
false,
),
Self::new_content_type_input(
window,
cx,
"Password",
InputContentType::Password,
"Current password",
"current-password",
true,
),
Self::new_content_type_input(
window,
cx,
"New password",
InputContentType::NewPassword,
"New password",
"new-password",
true,
),
Self::new_content_type_input(
window,
cx,
"One-time code",
InputContentType::OneTimeCode,
"123456",
"123456",
false,
),
Self::new_content_type_input(
window,
cx,
"Email",
InputContentType::EmailAddress,
"Email address",
"jane.doe@example.com",
false,
),
Self::new_content_type_input(
window,
cx,
"Telephone",
InputContentType::TelephoneNumber,
"Telephone number",
"+1 415 555 0198",
false,
),
Self::new_content_type_input(
window,
cx,
"URL",
InputContentType::Url,
"Website URL",
"https://example.com",
false,
),
Self::new_content_type_input(
window,
cx,
"Credit card number",
InputContentType::CreditCardNumber,
"Card number",
"4242 4242 4242 4242",
false,
),
Self::new_content_type_input(
window,
cx,
"Credit card expiration",
InputContentType::CreditCardExpiration,
"MM/YY",
"12/28",
false,
),
Self::new_content_type_input(
window,
cx,
"Credit card security code",
InputContentType::CreditCardSecurityCode,
"CVC",
"123",
true,
),
Self::new_content_type_input(
window,
cx,
"Postal code",
InputContentType::PostalCode,
"Postal code",
"94107",
false,
),
];
let _subscriptions = vec![
cx.subscribe_in(&input1, window, Self::on_input_event),
cx.subscribe_in(&input2, window, Self::on_input_event),
@@ -172,10 +291,60 @@ impl InputStory {
color_input,
input_text_centered,
input_text_right,
content_type_inputs,
_subscriptions,
}
}
fn new_content_type_input(
window: &mut Window,
cx: &mut Context<Self>,
label: &'static str,
content_type: InputContentType,
placeholder: &'static str,
default_value: &'static str,
masked: bool,
) -> ContentTypeInput {
let input = cx.new(|cx| {
let state = InputState::new(window, cx)
.placeholder(placeholder)
.default_value(default_value);
if masked { state.masked(true) } else { state }
});
ContentTypeInput {
label,
content_type,
input,
mask_toggle: masked,
}
}
fn render_content_type_input(item: &ContentTypeInput) -> impl IntoElement {
let input = Input::new(&item.input)
.content_type(item.content_type)
.flex_1();
let input = if item.mask_toggle {
input.mask_toggle()
} else {
input
};
h_flex()
.w_full()
.items_center()
.gap_3()
.child(
Label::new(item.label)
.w_48()
.flex_shrink_0()
.text_sm()
.whitespace_nowrap(),
)
.child(input)
}
fn on_input_event(
&mut self,
state: &Entity<InputState>,
@@ -221,13 +390,25 @@ impl Render for InputStory {
section("Normal Input")
.max_w_md()
.child(Input::new(&self.input1).cleanable(true))
.child(Input::new(&self.input2)),
.child(Input::new(&self.input2).role(Role::EmailInput)),
)
.child(
section("Input State")
.max_w_md()
.child(Input::new(&self.disabled_input).disabled(true))
.child(Input::new(&self.mask_input).mask_toggle().cleanable(true)),
.child(
Input::new(&self.mask_input)
.content_type(InputContentType::Password)
.mask_toggle()
.cleanable(true),
),
)
.child(
section("Content Type").max_w_lg().children(
self.content_type_inputs
.iter()
.map(Self::render_content_type_input),
),
)
.child(
section("Text Align").max_w_lg().child(
+244
View File
@@ -0,0 +1,244 @@
use gpui::Window;
/// Semantic content type for an [`Input`](super::Input).
///
/// These variants mirror Swift's text content types.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputContentType {
/// A person's full name.
Name,
/// A name prefix, such as Mr. or Dr.
NamePrefix,
/// A person's given name.
GivenName,
/// A person's middle name.
MiddleName,
/// A person's family name.
FamilyName,
/// A name suffix, such as Jr. or PhD.
NameSuffix,
/// A nickname.
Nickname,
/// A job title.
JobTitle,
/// An organization or company name.
OrganizationName,
/// A location name.
Location,
/// A full street address.
FullStreetAddress,
/// The first line of a street address.
StreetAddressLine1,
/// The second line of a street address.
StreetAddressLine2,
/// A city or locality.
AddressCity,
/// A state, province, or region.
AddressState,
/// A combined city and state.
AddressCityAndState,
/// A sublocality, district, or neighborhood.
Sublocality,
/// A country name.
CountryName,
/// A postal or ZIP code.
PostalCode,
/// A telephone number.
TelephoneNumber,
/// An email address.
EmailAddress,
/// A URL.
Url,
/// A credit card number.
CreditCardNumber,
/// The full name on a credit card.
CreditCardName,
/// The given name on a credit card.
CreditCardGivenName,
/// The middle name on a credit card.
CreditCardMiddleName,
/// The family name on a credit card.
CreditCardFamilyName,
/// The security code on a credit card.
CreditCardSecurityCode,
/// A credit card expiration date.
CreditCardExpiration,
/// A credit card expiration month.
CreditCardExpirationMonth,
/// A credit card expiration year.
CreditCardExpirationYear,
/// A credit card type.
CreditCardType,
/// A username or account identifier.
Username,
/// The password for the account identified by the username field.
Password,
/// A new password, such as during sign up or password reset.
NewPassword,
/// A one-time verification code.
OneTimeCode,
/// A parcel shipment tracking number.
ShipmentTrackingNumber,
/// An airline flight number.
FlightNumber,
/// A date, time, or duration.
DateTime,
/// A birthdate.
Birthdate,
/// A birthdate day.
BirthdateDay,
/// A birthdate month.
BirthdateMonth,
/// A birthdate year.
BirthdateYear,
/// An eSIM EID.
CellularEid,
/// A cellular IMEI.
CellularImei,
}
impl InputContentType {
#[cfg(target_os = "macos")]
pub(crate) const fn ns_text_content_type(self) -> Option<&'static str> {
match self {
Self::Name => Some("name"),
Self::NamePrefix => Some("honorific-prefix"),
Self::GivenName => Some("given-name"),
Self::MiddleName => Some("additional-name"),
Self::FamilyName => Some("family-name"),
Self::NameSuffix => Some("honorific-suffix"),
Self::Nickname => Some("nickname"),
Self::JobTitle => Some("organization-title"),
Self::OrganizationName => Some("organization"),
Self::Location => Some("location"),
Self::FullStreetAddress => Some("street-address"),
Self::StreetAddressLine1 => Some("address-line1"),
Self::StreetAddressLine2 => Some("address-line2"),
Self::AddressCity => Some("address-level2"),
Self::AddressState => Some("address-level1"),
Self::AddressCityAndState => Some("address-level1+2"),
Self::Sublocality => Some("address-level3"),
Self::CountryName => Some("country-name"),
Self::PostalCode => Some("postal-code"),
Self::TelephoneNumber => Some("tel"),
Self::EmailAddress => Some("email"),
Self::Url => Some("url"),
Self::CreditCardNumber => Some("cc-number"),
Self::CreditCardName => Some("cc-name"),
Self::CreditCardGivenName => Some("cc-given-name"),
Self::CreditCardMiddleName => Some("cc-additional-name"),
Self::CreditCardFamilyName => Some("cc-family-name"),
Self::CreditCardSecurityCode => Some("cc-csc"),
Self::CreditCardExpiration => Some("cc-exp"),
Self::CreditCardExpirationMonth => Some("cc-exp-month"),
Self::CreditCardExpirationYear => Some("cc-exp-year"),
Self::CreditCardType => Some("cc-type"),
Self::Username => Some("username"),
Self::Password => Some("password"),
Self::NewPassword => Some("new-password"),
Self::OneTimeCode => Some("one-time-code"),
Self::ShipmentTrackingNumber => Some("shipment-tracking-number"),
Self::FlightNumber => Some("flight-number"),
Self::DateTime => Some("date-time"),
Self::Birthdate => Some("bday"),
Self::BirthdateDay => Some("bday-day"),
Self::BirthdateMonth => Some("bday-month"),
Self::BirthdateYear => Some("bday-year"),
Self::CellularEid | Self::CellularImei => None,
}
}
}
pub(super) fn sync_native_content_type(
window: &mut Window,
content_type: Option<InputContentType>,
disabled: bool,
) {
if disabled {
return;
}
#[cfg(target_os = "macos")]
super::native::set_text_content_type(window, content_type);
#[cfg(not(target_os = "macos"))]
let _ = (window, content_type);
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
#[test]
fn content_type_maps_to_ns_text_content_type_values() {
let content_types = [
(InputContentType::Name, Some("name")),
(InputContentType::NamePrefix, Some("honorific-prefix")),
(InputContentType::GivenName, Some("given-name")),
(InputContentType::MiddleName, Some("additional-name")),
(InputContentType::FamilyName, Some("family-name")),
(InputContentType::NameSuffix, Some("honorific-suffix")),
(InputContentType::Nickname, Some("nickname")),
(InputContentType::JobTitle, Some("organization-title")),
(InputContentType::OrganizationName, Some("organization")),
(InputContentType::Location, Some("location")),
(InputContentType::FullStreetAddress, Some("street-address")),
(InputContentType::StreetAddressLine1, Some("address-line1")),
(InputContentType::StreetAddressLine2, Some("address-line2")),
(InputContentType::AddressCity, Some("address-level2")),
(InputContentType::AddressState, Some("address-level1")),
(
InputContentType::AddressCityAndState,
Some("address-level1+2"),
),
(InputContentType::Sublocality, Some("address-level3")),
(InputContentType::CountryName, Some("country-name")),
(InputContentType::PostalCode, Some("postal-code")),
(InputContentType::TelephoneNumber, Some("tel")),
(InputContentType::EmailAddress, Some("email")),
(InputContentType::Url, Some("url")),
(InputContentType::CreditCardNumber, Some("cc-number")),
(InputContentType::CreditCardName, Some("cc-name")),
(InputContentType::CreditCardGivenName, Some("cc-given-name")),
(
InputContentType::CreditCardMiddleName,
Some("cc-additional-name"),
),
(
InputContentType::CreditCardFamilyName,
Some("cc-family-name"),
),
(InputContentType::CreditCardSecurityCode, Some("cc-csc")),
(InputContentType::CreditCardExpiration, Some("cc-exp")),
(
InputContentType::CreditCardExpirationMonth,
Some("cc-exp-month"),
),
(
InputContentType::CreditCardExpirationYear,
Some("cc-exp-year"),
),
(InputContentType::CreditCardType, Some("cc-type")),
(InputContentType::Username, Some("username")),
(InputContentType::Password, Some("password")),
(InputContentType::NewPassword, Some("new-password")),
(InputContentType::OneTimeCode, Some("one-time-code")),
(
InputContentType::ShipmentTrackingNumber,
Some("shipment-tracking-number"),
),
(InputContentType::FlightNumber, Some("flight-number")),
(InputContentType::DateTime, Some("date-time")),
(InputContentType::Birthdate, Some("bday")),
(InputContentType::BirthdateDay, Some("bday-day")),
(InputContentType::BirthdateMonth, Some("bday-month")),
(InputContentType::BirthdateYear, Some("bday-year")),
(InputContentType::CellularEid, None),
(InputContentType::CellularImei, None),
];
for (content_type, native_value) in content_types {
assert_eq!(content_type.ns_text_content_type(), native_value);
}
}
}
+224 -9
View File
@@ -3,7 +3,7 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity, Hsla, InteractiveElement as _,
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, Role,
IntoElement, MouseButton, MouseDownEvent, ParentElement as _, Rems, RenderOnce, Role,
StatefulInteractiveElement as _, StyleRefinement, Styled, TextAlign, Window, div, px, relative,
};
@@ -16,7 +16,9 @@ use crate::{IconName, Size};
use crate::{Selectable, StyledExt, h_flex};
use crate::{Sizable, StyleSized};
use super::{InputState, element::EditorScrollbar};
use super::{
InputContentType, InputState, content_type::sync_native_content_type, element::EditorScrollbar,
};
/// Returns `(background, foreground)` colors for input-like components.
pub(crate) fn input_style(disabled: bool, cx: &App) -> (Hsla, Hsla) {
@@ -47,6 +49,8 @@ pub struct Input {
focus_bordered: bool,
tab_index: isize,
selected: bool,
content_type: Option<InputContentType>,
role: Option<Role>,
/// An optional context menu builder to allow a custom context menu on the input.
///
@@ -90,6 +94,8 @@ impl Input {
focus_bordered: true,
tab_index: 0,
selected: false,
content_type: None,
role: None,
context_menu_builder: None,
}
}
@@ -146,6 +152,23 @@ impl Input {
self
}
/// Set the semantic content type for password managers and autofill.
///
/// This is a component-level semantic hint. It does not change the text
/// value or masked rendering state.
pub fn content_type(mut self, content_type: InputContentType) -> Self {
self.content_type = Some(content_type);
self
}
/// Override the accessible role for the input.
///
/// If unset, the role is inferred from multi-line mode and content type.
pub fn role(mut self, role: Role) -> Self {
self.role = Some(role);
self
}
/// Set to disable the input field.
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
@@ -190,6 +213,81 @@ impl Input {
})
}
fn mouse_down_handler(
state: Entity<InputState>,
content_type: Option<InputContentType>,
disabled: bool,
) -> impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static {
move |event, window, cx| {
sync_native_content_type(window, content_type, disabled);
state.update(cx, |state, cx| state.on_mouse_down(event, window, cx));
}
}
fn accessibility_role(
is_multi_line: bool,
content_type: Option<InputContentType>,
role: Option<Role>,
) -> Role {
if let Some(role) = role {
return role;
}
if is_multi_line {
return Role::MultilineTextInput;
}
match content_type {
None => Role::TextInput,
Some(InputContentType::TelephoneNumber) => Role::PhoneNumberInput,
Some(InputContentType::EmailAddress) => Role::EmailInput,
Some(InputContentType::Url) => Role::UrlInput,
Some(InputContentType::Password | InputContentType::NewPassword) => Role::PasswordInput,
Some(InputContentType::DateTime) => Role::DateTimeInput,
Some(InputContentType::Birthdate) => Role::DateInput,
Some(
InputContentType::Name
| InputContentType::NamePrefix
| InputContentType::GivenName
| InputContentType::MiddleName
| InputContentType::FamilyName
| InputContentType::NameSuffix
| InputContentType::Nickname
| InputContentType::JobTitle
| InputContentType::OrganizationName
| InputContentType::Location
| InputContentType::FullStreetAddress
| InputContentType::StreetAddressLine1
| InputContentType::StreetAddressLine2
| InputContentType::AddressCity
| InputContentType::AddressState
| InputContentType::AddressCityAndState
| InputContentType::Sublocality
| InputContentType::CountryName
| InputContentType::PostalCode
| InputContentType::CreditCardNumber
| InputContentType::CreditCardName
| InputContentType::CreditCardGivenName
| InputContentType::CreditCardMiddleName
| InputContentType::CreditCardFamilyName
| InputContentType::CreditCardSecurityCode
| InputContentType::CreditCardExpiration
| InputContentType::CreditCardExpirationMonth
| InputContentType::CreditCardExpirationYear
| InputContentType::CreditCardType
| InputContentType::Username
| InputContentType::OneTimeCode
| InputContentType::ShipmentTrackingNumber
| InputContentType::FlightNumber
| InputContentType::BirthdateDay
| InputContentType::BirthdateMonth
| InputContentType::BirthdateYear
| InputContentType::CellularEid
| InputContentType::CellularImei,
) => Role::TextInput,
}
}
/// This method must after the refine_style.
fn render_editor(
paddings: EdgesRefinement<DefiniteLength>,
@@ -258,7 +356,15 @@ impl RenderOnce for Input {
});
let state = self.state.read(cx);
let content_type = self.content_type;
let disabled = self.disabled;
let is_multi_line = state.mode.is_multi_line();
let accessibility_role = Self::accessibility_role(is_multi_line, content_type, self.role);
let focused = state.focus_handle.is_focused(window) && !state.disabled;
if focused {
sync_native_content_type(window, content_type, state.disabled);
}
let gap_x = match self.size {
Size::Small => px(4.),
Size::Large => px(8.),
@@ -289,11 +395,7 @@ impl RenderOnce for Input {
div()
.id(("input", self.state.entity_id()))
.role(if state.mode.is_multi_line() {
Role::MultilineTextInput
} else {
Role::TextInput
})
.role(accessibility_role)
.flex()
.key_context(crate::input::CONTEXT)
.track_focus(&state.focus_handle.clone())
@@ -361,11 +463,11 @@ impl RenderOnce for Input {
.on_key_down(window.listener_for(&self.state, InputState::on_key_down))
.on_mouse_down(
MouseButton::Left,
window.listener_for(&self.state, InputState::on_mouse_down),
Self::mouse_down_handler(self.state.clone(), content_type, disabled),
)
.on_mouse_down(
MouseButton::Right,
window.listener_for(&self.state, InputState::on_mouse_down),
Self::mouse_down_handler(self.state.clone(), content_type, disabled),
)
.on_mouse_up(
MouseButton::Left,
@@ -445,3 +547,116 @@ impl RenderOnce for Input {
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn content_types_map_to_accessibility_roles() {
let cases = [
(None, Role::TextInput),
(Some(InputContentType::Name), Role::TextInput),
(Some(InputContentType::NamePrefix), Role::TextInput),
(Some(InputContentType::GivenName), Role::TextInput),
(Some(InputContentType::MiddleName), Role::TextInput),
(Some(InputContentType::FamilyName), Role::TextInput),
(Some(InputContentType::NameSuffix), Role::TextInput),
(Some(InputContentType::Nickname), Role::TextInput),
(Some(InputContentType::JobTitle), Role::TextInput),
(Some(InputContentType::OrganizationName), Role::TextInput),
(Some(InputContentType::Location), Role::TextInput),
(Some(InputContentType::FullStreetAddress), Role::TextInput),
(Some(InputContentType::StreetAddressLine1), Role::TextInput),
(Some(InputContentType::StreetAddressLine2), Role::TextInput),
(Some(InputContentType::AddressCity), Role::TextInput),
(Some(InputContentType::AddressState), Role::TextInput),
(Some(InputContentType::AddressCityAndState), Role::TextInput),
(Some(InputContentType::Sublocality), Role::TextInput),
(Some(InputContentType::CountryName), Role::TextInput),
(Some(InputContentType::PostalCode), Role::TextInput),
(
Some(InputContentType::TelephoneNumber),
Role::PhoneNumberInput,
),
(Some(InputContentType::EmailAddress), Role::EmailInput),
(Some(InputContentType::Url), Role::UrlInput),
(Some(InputContentType::CreditCardNumber), Role::TextInput),
(Some(InputContentType::CreditCardName), Role::TextInput),
(Some(InputContentType::CreditCardGivenName), Role::TextInput),
(
Some(InputContentType::CreditCardMiddleName),
Role::TextInput,
),
(
Some(InputContentType::CreditCardFamilyName),
Role::TextInput,
),
(
Some(InputContentType::CreditCardSecurityCode),
Role::TextInput,
),
(
Some(InputContentType::CreditCardExpiration),
Role::TextInput,
),
(
Some(InputContentType::CreditCardExpirationMonth),
Role::TextInput,
),
(
Some(InputContentType::CreditCardExpirationYear),
Role::TextInput,
),
(Some(InputContentType::CreditCardType), Role::TextInput),
(Some(InputContentType::Username), Role::TextInput),
(Some(InputContentType::Password), Role::PasswordInput),
(Some(InputContentType::NewPassword), Role::PasswordInput),
(Some(InputContentType::OneTimeCode), Role::TextInput),
(
Some(InputContentType::ShipmentTrackingNumber),
Role::TextInput,
),
(Some(InputContentType::FlightNumber), Role::TextInput),
(Some(InputContentType::DateTime), Role::DateTimeInput),
(Some(InputContentType::Birthdate), Role::DateInput),
(Some(InputContentType::BirthdateDay), Role::TextInput),
(Some(InputContentType::BirthdateMonth), Role::TextInput),
(Some(InputContentType::BirthdateYear), Role::TextInput),
(Some(InputContentType::CellularEid), Role::TextInput),
(Some(InputContentType::CellularImei), Role::TextInput),
];
for (content_type, role) in cases {
assert_eq!(Input::accessibility_role(false, content_type, None), role);
}
}
#[test]
fn multiline_inputs_keep_multiline_accessibility_role() {
assert_eq!(
Input::accessibility_role(true, Some(InputContentType::Password), None),
Role::MultilineTextInput
);
}
#[test]
fn explicit_accessibility_role_overrides_defaults() {
assert_eq!(
Input::accessibility_role(
false,
Some(InputContentType::Password),
Some(Role::TextInput)
),
Role::TextInput
);
assert_eq!(
Input::accessibility_role(
true,
Some(InputContentType::Password),
Some(Role::TextInput)
),
Role::TextInput
);
}
}
+4
View File
@@ -4,6 +4,7 @@ pub(super) const MASK_CHAR: char = '•';
mod blink_cursor;
mod change;
mod clear_button;
mod content_type;
mod cursor;
mod display_map;
mod element;
@@ -13,6 +14,8 @@ mod lsp;
mod mask_pattern;
mod mode;
mod movement;
#[cfg(target_os = "macos")]
mod native;
mod number_input;
mod otp_input;
pub(crate) mod popovers;
@@ -22,6 +25,7 @@ mod selection;
mod state;
pub(crate) use clear_button::*;
pub use content_type::*;
pub use cursor::*;
#[cfg(not(feature = "tree-sitter"))]
pub use display_map::Tree;
+112
View File
@@ -0,0 +1,112 @@
#[cfg(target_os = "macos")]
mod macos {
use std::{cell::RefCell, collections::HashMap, mem, ptr, sync::Once};
use gpui::Window;
use objc2::{
ffi, msg_send,
rc::Retained,
runtime::{AnyObject, AnyProtocol, Imp, Sel},
sel,
};
use objc2_foundation::NSString;
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use crate::input::InputContentType;
static INSTALL_TEXT_CONTENT: Once = Once::new();
thread_local! {
static CONTENT_TYPES: RefCell<HashMap<usize, Retained<NSString>>> =
RefCell::new(HashMap::new());
}
pub(crate) fn set_text_content_type(window: &Window, content_type: Option<InputContentType>) {
let Some(view) = ns_view(window) else {
return;
};
INSTALL_TEXT_CONTENT.call_once(|| install_text_content(view));
if view
.class()
.instance_method(sel!(setContentType:))
.is_none()
{
return;
}
let ns_content_type = content_type
.and_then(InputContentType::ns_text_content_type)
.map(NSString::from_str);
let ns_content_type = ns_content_type.as_ref().map_or(ptr::null_mut(), |value| {
Retained::as_ptr(value).cast_mut().cast::<AnyObject>()
});
unsafe {
let _: () = msg_send![view, setContentType: ns_content_type];
}
}
fn ns_view(window: &Window) -> Option<&AnyObject> {
let handle = HasWindowHandle::window_handle(window).ok()?;
let RawWindowHandle::AppKit(handle) = handle.as_raw() else {
return None;
};
Some(unsafe { &*(handle.ns_view.as_ptr() as *const AnyObject) })
}
fn install_text_content(view: &AnyObject) {
let class = view.class();
let class = class as *const _ as *mut _;
unsafe {
let protocol = AnyProtocol::get(c"NSTextContent");
if let Some(protocol) = protocol {
ffi::class_addProtocol(class, protocol);
}
let content_type_imp: Imp = mem::transmute(
content_type as unsafe extern "C-unwind" fn(&AnyObject, Sel) -> *mut AnyObject,
);
let set_content_type_imp: Imp = mem::transmute(
set_content_type as unsafe extern "C-unwind" fn(&AnyObject, Sel, *mut AnyObject),
);
ffi::class_addMethod(class, sel!(contentType), content_type_imp, c"@@:".as_ptr());
ffi::class_addMethod(
class,
sel!(setContentType:),
set_content_type_imp,
c"v@:@".as_ptr(),
);
}
}
unsafe extern "C-unwind" fn content_type(this: &AnyObject, _: Sel) -> *mut AnyObject {
let key = this as *const _ as usize;
CONTENT_TYPES.with(|content_types| {
content_types
.borrow()
.get(&key)
.map_or(ptr::null_mut(), |value| {
Retained::as_ptr(value).cast_mut().cast::<AnyObject>()
})
})
}
unsafe extern "C-unwind" fn set_content_type(this: &AnyObject, _: Sel, value: *mut AnyObject) {
let key = this as *const _ as usize;
CONTENT_TYPES.with(|content_types| {
let mut content_types = content_types.borrow_mut();
if value.is_null() {
content_types.remove(&key);
} else if let Some(value) = unsafe { Retained::retain(value.cast::<NSString>()) } {
content_types.insert(key, value);
}
});
}
}
#[cfg(target_os = "macos")]
pub(crate) use macos::set_text_content_type;
+1 -1
View File
@@ -11,7 +11,7 @@ mod icon;
mod index_path;
#[cfg(any(feature = "inspector", debug_assertions))]
mod inspector;
#[cfg(target_os = "macos")]
#[cfg(all(target_os = "macos", not(test)))]
mod macos_accessibility;
mod root;
mod styled;
+1 -1
View File
@@ -94,7 +94,7 @@ impl ActiveDialog {
impl Root {
/// Create a new Root view.
pub fn new(view: impl Into<AnyView>, window: &mut Window, cx: &mut Context<Self>) -> Self {
#[cfg(target_os = "macos")]
#[cfg(all(target_os = "macos", not(test)))]
crate::macos_accessibility::install_window_hit_test_forwarder(window);
Self {
+1
View File
@@ -86,6 +86,7 @@ let input = cx.new(|cx|
);
Input::new(&input)
.content_type(InputContentType::Password)
.mask_toggle() // Shows toggle button to reveal password
```
+1
View File
@@ -83,6 +83,7 @@ let input = cx.new(|cx|
);
Input::new(&input)
.content_type(InputContentType::Password)
.mask_toggle()
```
@@ -135,6 +135,7 @@ Input::new(&input).cleanable(true) // clear button
Input::new(&input).disabled(true)
Input::new(&input).prefix(Icon::new(IconName::Search).small())
Input::new(&input).suffix(Button::new("b").ghost().icon(IconName::X).xsmall())
Input::new(&input).content_type(InputContentType::Password)
Input::new(&input).mask_toggle() // password reveal toggle
Input::new(&input).appearance(false) // remove default border/bg