gpui_kit_material/components/
text.rs1use crate::color::ColorRole;
3use crate::icons::{Icon, IconName};
4use crate::theme::color;
5use crate::tokens::{layout::*, shape::RADIUS_CARD, typography::*};
6use gpui::{prelude::*, *};
7
8pub fn accessible_text(id: impl Into<ElementId>, value: impl Into<SharedString>) -> Stateful<Div> {
12 let value = value.into();
13 let label = spoken(&value);
14 div()
15 .id(id)
16 .role(Role::Label)
17 .aria_label(label)
18 .child(value)
19}
20
21fn spoken(value: &SharedString) -> SharedString {
25 if value.contains('\u{2060}') {
26 SharedString::from(value.replace('\u{2060}', ""))
27 } else {
28 value.clone()
29 }
30}
31
32pub fn error_text(id: impl Into<ElementId>, message: impl Into<SharedString>) -> Stateful<Div> {
35 accessible_text(id, message)
36 .role(Role::Alert)
37 .text_color(color(ColorRole::ERROR))
38}
39
40pub fn semantic_label(
43 id: impl Into<ElementId>,
44 label: impl Into<SharedString>,
45 icon: impl IntoElement,
46) -> Div {
47 div()
48 .flex()
49 .items_center()
50 .min_w_0()
51 .gap(ICON_GAP)
52 .child(
53 div()
54 .flex()
55 .items_center()
56 .justify_center()
57 .size(ICON_SLOT)
58 .flex_shrink_0()
59 .child(icon),
60 )
61 .child(
62 accessible_text(id, label)
63 .min_w_0()
64 .whitespace_normal()
65 .text_size(TEXT_BODY)
66 .font_weight(FontWeight::MEDIUM),
67 )
68}
69
70pub fn supporting_info(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Div {
74 div()
75 .flex()
76 .w_full()
77 .min_w_0()
78 .items_start()
79 .gap(ICON_GAP)
80 .child(
81 div()
82 .flex()
83 .justify_center()
84 .w(ICON_SLOT)
85 .h(rem(18.))
86 .items_center()
87 .flex_shrink_0()
88 .child(
89 IconName::Info
90 .icon()
91 .size(rem(16.))
92 .color(color(ColorRole::ON_SURFACE_VARIANT)),
93 ),
94 )
95 .child(
96 accessible_text(id, text)
97 .flex_1()
98 .min_w_0()
99 .whitespace_normal()
100 .text_size(TEXT_AUX)
101 .line_height(rem(18.))
102 .text_color(color(ColorRole::ON_SURFACE_VARIANT)),
103 )
104}
105
106pub fn info_callout(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Div {
109 div()
110 .flex()
111 .w_full()
112 .min_w_0()
113 .items_start()
114 .gap(rem(10.))
115 .px(rem(16.))
116 .py(rem(12.))
117 .rounded(RADIUS_CARD)
118 .bg(color(ColorRole::SURFACE_CONTAINER_HIGHEST))
119 .child(
120 div().mt(rem(1.)).child(
121 IconName::Info
122 .icon()
123 .size(rem(18.))
124 .color(color(ColorRole::ON_SURFACE_VARIANT)),
125 ),
126 )
127 .child(
128 accessible_text(id, text)
129 .flex_1()
130 .min_w_0()
131 .whitespace_normal()
132 .text_size(TEXT_AUX)
133 .line_height(rem(18.))
134 .text_color(color(ColorRole::ON_SURFACE_VARIANT)),
135 )
136}
137
138pub fn page_heading(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Div {
141 div().flex().items_center().min_w_0().child(
142 accessible_text(id, title)
143 .min_w_0()
144 .whitespace_normal()
145 .role(Role::Heading)
146 .text_size(TEXT_HEADLINE)
147 .line_height(HEADLINE_LINE_HEIGHT)
148 .font_weight(FontWeight::NORMAL),
149 )
150}
151
152pub fn section_title(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Stateful<Div> {
155 accessible_text(id, title)
156 .role(Role::Heading)
157 .text_size(TEXT_BODY)
158 .line_height(rem(20.))
159 .font_weight(FontWeight::MEDIUM)
160 .text_color(color(ColorRole::ON_SURFACE_VARIANT))
161}
162
163pub fn section_heading(
165 id: impl Into<ElementId>,
166 icon: impl Into<Icon>,
167 title: impl Into<SharedString>,
168) -> Div {
169 div()
170 .flex()
171 .min_w_0()
172 .items_center()
173 .gap(ICON_GAP)
174 .text_color(color(ColorRole::ON_SURFACE_VARIANT))
175 .child(icon.into().size(rem(18.)))
176 .child(section_title(id, title))
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use core::prelude::v1::test;
183
184 #[test]
185 fn spoken_text_drops_word_joiners_only() {
186 let joined = SharedString::from("Text\u{2060}to\u{2060}speech");
187 assert_eq!(spoken(&joined).as_ref(), "Texttospeech");
188 let plain = SharedString::from("Plain words");
189 assert_eq!(spoken(&plain), plain);
190 }
191}