mirror of
https://github.com/librekeys/gpui-component.git
synced 2026-07-28 08:00:57 -07:00
input: Support content type for Input. (#2495)
This commit is contained in:
@@ -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.
|
||||
///
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -86,6 +86,7 @@ let input = cx.new(|cx|
|
||||
);
|
||||
|
||||
Input::new(&input)
|
||||
.content_type(InputContentType::Password)
|
||||
.mask_toggle() // Shows toggle button to reveal password
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user