1use std::path::Path;
4
5use objc2::rc::Retained;
6use objc2::runtime::ProtocolObject;
7use objc2::{AnyThread, MainThreadMarker, MainThreadOnly, Message};
8use objc2_app_kit::{
9 NSAccessibility, NSBezelStyle, NSBorderType, NSBox, NSBoxType, NSButton, NSButtonType,
10 NSCellImagePosition, NSColor, NSControlSize, NSFont, NSFontTextStyleBody,
11 NSFontTextStyleCaption1, NSFontTextStyleHeadline, NSFontTextStyleLargeTitle,
12 NSFontTextStyleTitle1, NSImage, NSImageScaling, NSImageView, NSPopUpButton,
13 NSProgressIndicator, NSProgressIndicatorStyle, NSScrollView, NSScroller, NSScrollerStyle,
14 NSSlider, NSTextAlignment, NSTextField, NSTextFieldBezelStyle, NSTextView, NSTitlePosition,
15 NSUserInterfaceLayoutDirection, NSView, NSVisualEffectBlendingMode, NSVisualEffectMaterial,
16 NSVisualEffectState, NSVisualEffectView,
17};
18use objc2_foundation::{NSDictionary, NSRect, NSSize, NSString, ns_string};
19
20use nifty_runtime::{
21 ButtonStyle, ClickHandler, DropHandler, Frame, Icon, ImageSize, LabelStyle, MenuEntry,
22 MenuHandler, NumberHandler, Size, Surface, TextAlign, TextColor, TextHandler, TextStyle,
23 ToggleHandler,
24};
25
26use crate::classes::{
27 ClickTarget, FlippedView, NumberTarget, PickTarget, TextFieldDelegate, TextViewDelegate,
28 ToggleTarget, control_state,
29};
30use crate::context_menu::MenuArea;
31use crate::convert::{to_double, to_ns_rect, to_ns_size, to_points, to_size};
32use crate::drop::DropArea;
33use crate::icons::symbol;
34use crate::nav_item::NavItem;
35use crate::switch::SwitchRow;
36
37const MIN_TEXT_FIELD_WIDTH: f32 = 200.0;
39const DIVIDER_SIZE: Size = Size::new(1.0, 1.0);
40const MIN_TRACK_WIDTH: f32 = 100.0;
42const FULL_PROGRESS: f64 = 100.0;
44const TEXT_AREA_INSET: NSSize = NSSize::new(2.0, 4.0);
46const NO_ITEM: isize = -1;
48const CARD_CORNER_RADIUS: f64 = 10.0;
50const CARD_BORDER_WIDTH: f64 = 1.0;
51
52#[derive(Debug)]
53pub struct MacWidget(WidgetKind);
54
55#[derive(Debug)]
57enum WidgetKind {
58 Label(Retained<NSTextField>),
59 Button {
60 button: Retained<NSButton>,
61 target: Retained<ClickTarget>,
62 icon_only: bool,
64 },
65 TextField {
66 field: Retained<NSTextField>,
67 delegate: Retained<TextFieldDelegate>,
68 },
69 TextArea {
71 scroll: Retained<NSScrollView>,
72 text_view: Retained<NSTextView>,
73 delegate: Retained<TextViewDelegate>,
74 lines: u32,
75 },
76 Checkbox {
77 checkbox: Retained<NSButton>,
78 target: Retained<ToggleTarget>,
79 },
80 Switch(SwitchRow),
81 Slider {
82 slider: Retained<NSSlider>,
83 target: Retained<NumberTarget>,
84 },
85 Picker {
86 popup: Retained<NSPopUpButton>,
87 target: Retained<PickTarget>,
88 },
89 NavItem(NavItem),
90 Progress(Retained<NSProgressIndicator>),
91 Image {
92 view: Retained<NSImageView>,
93 size: ImageSize,
94 },
95 Divider(Retained<NSBox>),
96 ContextMenu(Retained<MenuArea>),
98 DropTarget(Retained<DropArea>),
100 Surface(Retained<NSView>),
101 Scroll {
103 view: Retained<NSScrollView>,
104 content: Retained<FlippedView>,
105 },
106}
107
108impl MacWidget {
109 pub(crate) fn label(mtm: MainThreadMarker, text: &str, style: LabelStyle) -> Self {
110 let label = NSTextField::labelWithString(&NSString::from_str(text), mtm);
111 label.setFont(Some(&system_font(style.font)));
112 label.setTextColor(Some(&text_color(style.color)));
113 label.setAlignment(text_alignment(
114 style.align,
115 label.userInterfaceLayoutDirection(),
116 ));
117 label.setSelectable(style.selectable);
118 Self(WidgetKind::Label(label))
119 }
120
121 pub(crate) fn button(
123 mtm: MainThreadMarker,
124 title: &str,
125 (style, icon): (ButtonStyle, Icon),
126 selected: Option<bool>,
127 on_click: ClickHandler,
128 ) -> Self {
129 let target = match selected {
130 Some(selected) => ClickTarget::selecting(mtm, on_click, selected),
131 None => ClickTarget::new(mtm, on_click),
132 };
133 let button = click_button(mtm, title, &target);
134 if let Some(selected) = selected {
135 button.setButtonType(NSButtonType::PushOnPushOff);
136 button.setState(control_state(selected));
137 }
138 if let Some(symbol) = symbol(icon) {
139 button.setImage(Some(&symbol));
140 button.setImagePosition(NSCellImagePosition::ImageLeading);
141 }
142 let icon_only = style == ButtonStyle::Icon;
143 match style {
144 ButtonStyle::Primary => button.setKeyEquivalent(ns_string!("\r")),
146 ButtonStyle::Plain => {
147 border_on_hover(&button);
148 button.setContentTintColor(Some(&NSColor::controlAccentColor()));
149 }
150 ButtonStyle::Icon => {
151 border_on_hover(&button);
152 button.setImagePosition(NSCellImagePosition::ImageOnly);
153 button.setContentTintColor(Some(&NSColor::secondaryLabelColor()));
154 show_title_beside(&button, &NSString::from_str(title));
155 }
156 ButtonStyle::Normal => {}
157 }
158 Self(WidgetKind::Button {
159 button,
160 target,
161 icon_only,
162 })
163 }
164
165 pub(crate) fn nav_item(
166 mtm: MainThreadMarker,
167 title: &str,
168 icon: Icon,
169 badge: &str,
170 selected: bool,
171 on_click: ClickHandler,
172 ) -> Self {
173 Self(WidgetKind::NavItem(NavItem::new(
174 mtm, title, icon, badge, selected, on_click,
175 )))
176 }
177
178 pub(crate) fn text_field(
179 mtm: MainThreadMarker,
180 text: &str,
181 placeholder: &str,
182 on_change: TextHandler,
183 on_submit: Option<ClickHandler>,
184 ) -> Self {
185 let field = NSTextField::textFieldWithString(&NSString::from_str(text), mtm);
186 field.setBezelStyle(NSTextFieldBezelStyle::RoundedBezel);
187 field.setPlaceholderString(Some(&NSString::from_str(placeholder)));
188 let delegate = TextFieldDelegate::new(mtm, &field, on_change, on_submit);
189 unsafe { field.setDelegate(Some(ProtocolObject::from_ref(&*delegate))) };
191 Self(WidgetKind::TextField { field, delegate })
192 }
193
194 pub(crate) fn text_area(
196 mtm: MainThreadMarker,
197 text: &str,
198 lines: u32,
199 on_change: TextHandler,
200 ) -> Self {
201 let scroll = NSTextView::scrollableTextView(mtm);
202 scroll.setBorderType(NSBorderType::BezelBorder);
203 let text_view = scroll
204 .documentView()
205 .and_then(|view| view.downcast::<NSTextView>().ok())
206 .expect("a scrollable text view holds a text view");
207 text_view.setRichText(false);
208 text_view.setAllowsUndo(true);
209 text_view.setFont(Some(&system_font(TextStyle::Body)));
210 text_view.setTextContainerInset(TEXT_AREA_INSET);
211 text_view.setString(&NSString::from_str(text));
212 let delegate = TextViewDelegate::new(mtm, &text_view, on_change);
213 text_view.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
214 Self(WidgetKind::TextArea {
215 scroll,
216 text_view,
217 delegate,
218 lines,
219 })
220 }
221
222 pub(crate) fn checkbox(
223 mtm: MainThreadMarker,
224 title: &str,
225 checked: bool,
226 on_toggle: ToggleHandler,
227 ) -> Self {
228 let target = ToggleTarget::new(mtm, on_toggle);
229 let checkbox = unsafe {
231 NSButton::checkboxWithTitle_target_action(
232 &NSString::from_str(title),
233 Some(&target),
234 Some(ToggleTarget::action()),
235 mtm,
236 )
237 };
238 checkbox.setState(control_state(checked));
239 Self(WidgetKind::Checkbox { checkbox, target })
240 }
241
242 pub(crate) fn progress(mtm: MainThreadMarker, percent: Option<i64>) -> Self {
244 let bar = NSProgressIndicator::new(mtm);
245 bar.setStyle(NSProgressIndicatorStyle::Bar);
246 bar.setMinValue(0.0);
247 bar.setMaxValue(FULL_PROGRESS);
248 bar.setIndeterminate(percent.is_none());
249 match percent {
250 Some(percent) => bar.setDoubleValue(to_double(percent)),
251 None => unsafe { bar.startAnimation(None) },
253 }
254 Self(WidgetKind::Progress(bar))
255 }
256
257 pub(crate) fn switch(
258 mtm: MainThreadMarker,
259 title: &str,
260 on: bool,
261 on_toggle: ToggleHandler,
262 ) -> Self {
263 Self(WidgetKind::Switch(SwitchRow::new(
264 mtm, title, on, on_toggle,
265 )))
266 }
267
268 pub(crate) fn slider(
270 mtm: MainThreadMarker,
271 value: i64,
272 min: i64,
273 max: i64,
274 on_change: NumberHandler,
275 ) -> Self {
276 let target = NumberTarget::new(mtm, value, on_change);
277 let slider = unsafe {
280 NSSlider::sliderWithValue_minValue_maxValue_target_action(
281 to_double(value),
282 to_double(min),
283 to_double(max),
284 Some(&target),
285 Some(NumberTarget::action()),
286 mtm,
287 )
288 };
289 slider.setContinuous(true);
290 Self(WidgetKind::Slider { slider, target })
291 }
292
293 pub(crate) fn picker(
294 mtm: MainThreadMarker,
295 options: &[String],
296 selected: &str,
297 on_pick: TextHandler,
298 ) -> Self {
299 let target = PickTarget::new(mtm, on_pick);
300 let popup =
301 NSPopUpButton::initWithFrame_pullsDown(NSPopUpButton::alloc(mtm), NSRect::ZERO, false);
302 unsafe {
305 popup.setTarget(Some(&target));
306 popup.setAction(Some(PickTarget::action()));
307 }
308 let picker = Self(WidgetKind::Picker { popup, target });
309 picker.set_options(options);
310 picker.set_text(selected);
311 picker
312 }
313
314 pub(crate) fn image(mtm: MainThreadMarker, path: Option<&Path>, size: ImageSize) -> Self {
316 let view = NSImageView::new(mtm);
317 view.setImageScaling(NSImageScaling::ScaleProportionallyUpOrDown);
318 let image = Self(WidgetKind::Image { view, size });
319 image.set_image(path);
320 image
321 }
322
323 pub(crate) fn set_image(&self, path: Option<&Path>) {
326 if let WidgetKind::Image { view, .. } = &self.0 {
327 let image = path.and_then(|path| {
328 let path = NSString::from_str(&path.to_string_lossy());
329 NSImage::initWithContentsOfFile(NSImage::alloc(), &path)
330 });
331 view.setImage(image.as_deref());
332 }
333 }
334
335 pub(crate) fn divider(mtm: MainThreadMarker) -> Self {
336 let divider = NSBox::new(mtm);
337 divider.setBoxType(NSBoxType::Separator);
338 Self(WidgetKind::Divider(divider))
339 }
340
341 pub(crate) fn context_menu(
342 mtm: MainThreadMarker,
343 entries: &[MenuEntry<usize>],
344 on_select: MenuHandler,
345 ) -> Self {
346 Self(WidgetKind::ContextMenu(MenuArea::new(
347 mtm, entries, on_select,
348 )))
349 }
350
351 pub(crate) fn drop_target(mtm: MainThreadMarker, on_drop: DropHandler) -> Self {
352 Self(WidgetKind::DropTarget(DropArea::new(mtm, on_drop)))
353 }
354
355 pub(crate) fn surface(mtm: MainThreadMarker, surface: Surface) -> Self {
356 let view = match surface {
357 Surface::Sidebar => {
358 Retained::into_super(behind_window(mtm, NSVisualEffectMaterial::Sidebar))
359 }
360 Surface::Content => {
361 Retained::into_super(panel(mtm, &NSColor::controlBackgroundColor()))
362 }
363 Surface::Card => {
364 let card = panel(mtm, &NSColor::controlBackgroundColor());
365 card.setCornerRadius(CARD_CORNER_RADIUS);
366 card.setBorderWidth(CARD_BORDER_WIDTH);
367 card.setBorderColor(&NSColor::separatorColor());
368 Retained::into_super(card)
369 }
370 Surface::None => unreachable!("the runtime only creates surfaces it draws"),
371 };
372 Self(WidgetKind::Surface(view))
373 }
374
375 pub(crate) fn scroll(mtm: MainThreadMarker) -> Self {
378 let view = NSScrollView::new(mtm);
379 view.setBorderType(NSBorderType::NoBorder);
380 view.setDrawsBackground(false);
381 view.setHasVerticalScroller(true);
382 view.setAutohidesScrollers(false);
386 let content = FlippedView::new(mtm);
387 view.setDocumentView(Some(&content));
388 Self(WidgetKind::Scroll { view, content })
389 }
390
391 pub(crate) fn scroll_bar_width(mtm: MainThreadMarker) -> f32 {
394 match NSScroller::preferredScrollerStyle(mtm) {
395 NSScrollerStyle::Overlay => 0.0,
396 style => to_points(NSScroller::scrollerWidthForControlSize_scrollerStyle(
397 NSControlSize::Regular,
398 style,
399 mtm,
400 )),
401 }
402 }
403
404 pub(crate) fn content_view(&self) -> &NSView {
409 match &self.0 {
410 WidgetKind::Scroll { content, .. } => content,
411 _ => panic!("only scroll areas hold content"),
412 }
413 }
414
415 pub(crate) fn set_scroll_content(&self, size: Size) {
416 if let WidgetKind::Scroll { content, .. } = &self.0 {
417 content.setFrameSize(to_ns_size(size));
418 }
419 }
420
421 pub(crate) fn view(&self) -> &NSView {
422 match &self.0 {
423 WidgetKind::Label(label) => label,
424 WidgetKind::Button { button, .. } => button,
425 WidgetKind::TextField { field, .. } => field,
426 WidgetKind::TextArea { scroll, .. } => scroll,
427 WidgetKind::Checkbox { checkbox, .. } => checkbox,
428 WidgetKind::Switch(row) => row.view(),
429 WidgetKind::Slider { slider, .. } => slider,
430 WidgetKind::Picker { popup, .. } => popup,
431 WidgetKind::NavItem(item) => item.view(),
432 WidgetKind::Progress(bar) => bar,
433 WidgetKind::Image { view, .. } => view,
434 WidgetKind::Divider(divider) => divider,
435 WidgetKind::ContextMenu(area) => area,
436 WidgetKind::DropTarget(area) => area,
437 WidgetKind::Surface(view) => view,
438 WidgetKind::Scroll { view, .. } => view,
439 }
440 }
441
442 pub(crate) fn set_text(&self, text: &str) {
443 match &self.0 {
444 WidgetKind::Label(label) => label.setStringValue(&NSString::from_str(text)),
445 WidgetKind::Button {
446 button, icon_only, ..
447 } => {
448 let text = NSString::from_str(text);
449 if *icon_only {
450 show_title_beside(button, &text);
451 } else {
452 button.setTitle(&text);
453 }
454 }
455 WidgetKind::NavItem(item) => item.set_title(text),
456 WidgetKind::Checkbox { checkbox, .. } => checkbox.setTitle(&NSString::from_str(text)),
457 WidgetKind::Switch(row) => row.set_title(text),
458 WidgetKind::Picker { popup, .. } => {
459 let index = (0..popup.numberOfItems())
460 .find(|&index| popup.itemTitleAtIndex(index).to_string() == text)
461 .unwrap_or(NO_ITEM);
462 popup.selectItemAtIndex(index);
463 }
464 WidgetKind::TextField { field, .. } => {
466 if field.stringValue().to_string() != text {
467 field.setStringValue(&NSString::from_str(text));
468 }
469 }
470 WidgetKind::TextArea { text_view, .. } => {
471 if text_view.string().to_string() != text {
472 text_view.setString(&NSString::from_str(text));
473 }
474 }
475 WidgetKind::Progress(_)
476 | WidgetKind::Image { .. }
477 | WidgetKind::Slider { .. }
478 | WidgetKind::Divider(_)
479 | WidgetKind::ContextMenu(_)
480 | WidgetKind::DropTarget(_)
481 | WidgetKind::Surface(_)
482 | WidgetKind::Scroll { .. } => {}
483 }
484 }
485
486 pub(crate) fn set_checked(&self, checked: bool) {
487 match &self.0 {
488 WidgetKind::Checkbox { checkbox, .. } => checkbox.setState(control_state(checked)),
489 WidgetKind::Button { button, target, .. } => {
490 target.set_selected(checked);
491 button.setState(control_state(checked));
492 }
493 WidgetKind::Switch(row) => row.set_on(checked),
494 WidgetKind::NavItem(item) => item.set_selected(checked),
495 _ => {}
496 }
497 }
498
499 pub(crate) fn set_badge(&self, badge: &str) {
500 if let WidgetKind::NavItem(item) = &self.0 {
501 item.set_badge(badge);
502 }
503 }
504
505 pub(crate) fn set_value(&self, value: i64) {
506 match &self.0 {
507 WidgetKind::Progress(bar) => bar.setDoubleValue(to_double(value)),
508 WidgetKind::Slider { slider, target } => {
509 target.shown(value);
510 slider.setDoubleValue(to_double(value));
511 }
512 _ => {}
513 }
514 }
515
516 pub(crate) fn set_options(&self, options: &[String]) {
519 let WidgetKind::Picker { popup, .. } = &self.0 else {
520 return;
521 };
522 let shown = popup.titleOfSelectedItem();
523 let menu = popup.menu().expect("a pop-up button has a menu");
524 menu.removeAllItems();
525 for option in options {
526 unsafe {
528 menu.addItemWithTitle_action_keyEquivalent(
529 &NSString::from_str(option),
530 None,
531 ns_string!(""),
532 );
533 }
534 }
535 self.set_text(&shown.map(|title| title.to_string()).unwrap_or_default());
536 }
537
538 pub(crate) fn measure(&self) -> Size {
539 match &self.0 {
540 WidgetKind::TextField { .. } => {
541 let fitting = to_size(self.view().fittingSize());
542 Size::new(fitting.width.max(MIN_TEXT_FIELD_WIDTH), fitting.height)
543 }
544 WidgetKind::TextArea {
545 text_view, lines, ..
546 } => text_area_size(text_view, *lines),
547 WidgetKind::NavItem(item) => item.measure(),
548 WidgetKind::Switch(row) => row.measure(),
549 WidgetKind::Progress(_) | WidgetKind::Slider { .. } => {
550 let fitting = to_size(self.view().fittingSize());
551 Size::new(fitting.width.max(MIN_TRACK_WIDTH), fitting.height)
552 }
553 WidgetKind::Image { view, size } => {
554 let natural = view
555 .image()
556 .map_or(Size::ZERO, |image| to_size(image.size()));
557 size.frame(natural)
558 }
559 WidgetKind::Divider(_) => DIVIDER_SIZE,
560 _ => to_size(self.view().fittingSize()),
561 }
562 }
563
564 pub(crate) fn set_frame(&self, frame: Frame) {
565 match &self.0 {
566 WidgetKind::NavItem(item) => item.set_frame(frame),
567 WidgetKind::Switch(row) => row.set_frame(frame),
568 _ => self.view().setFrame(to_ns_rect(frame)),
569 }
570 }
571
572 pub(crate) fn remove(self) {
576 self.view().removeFromSuperview();
577 match self.0 {
578 WidgetKind::Label(label) => release_after_event(label),
579 WidgetKind::NavItem(item) => item.remove(),
580 WidgetKind::Button { button, target, .. } => {
581 release_after_event(button);
582 release_after_event(target);
583 }
584 WidgetKind::TextField { field, delegate } => {
585 release_after_event(field);
586 release_after_event(delegate);
587 }
588 WidgetKind::TextArea {
589 scroll,
590 text_view,
591 delegate,
592 ..
593 } => {
594 release_after_event(scroll);
595 release_after_event(text_view);
596 release_after_event(delegate);
597 }
598 WidgetKind::Checkbox { checkbox, target } => {
599 release_after_event(checkbox);
600 release_after_event(target);
601 }
602 WidgetKind::Switch(row) => row.remove(),
603 WidgetKind::Slider { slider, target } => {
604 release_after_event(slider);
605 release_after_event(target);
606 }
607 WidgetKind::Picker { popup, target } => {
608 release_after_event(popup);
609 release_after_event(target);
610 }
611 WidgetKind::Progress(bar) => {
612 unsafe { bar.stopAnimation(None) };
614 release_after_event(bar);
615 }
616 WidgetKind::Image { view, .. } => release_after_event(view),
617 WidgetKind::Divider(divider) => release_after_event(divider),
618 WidgetKind::ContextMenu(area) => release_after_event(area),
619 WidgetKind::DropTarget(area) => release_after_event(area),
620 WidgetKind::Surface(view) => release_after_event(view),
621 WidgetKind::Scroll { view, content } => {
622 release_after_event(view);
623 release_after_event(content);
624 }
625 }
626 }
627}
628
629pub(crate) fn behind_window(
632 mtm: MainThreadMarker,
633 material: NSVisualEffectMaterial,
634) -> Retained<NSVisualEffectView> {
635 let view = NSVisualEffectView::new(mtm);
636 view.setMaterial(material);
637 view.setBlendingMode(NSVisualEffectBlendingMode::BehindWindow);
638 view.setState(NSVisualEffectState::FollowsWindowActiveState);
639 view
640}
641
642pub(crate) fn panel(mtm: MainThreadMarker, fill: &NSColor) -> Retained<NSBox> {
645 let panel = NSBox::new(mtm);
646 panel.setBoxType(NSBoxType::Custom);
647 panel.setTitlePosition(NSTitlePosition::NoTitle);
648 panel.setContentViewMargins(NSSize::ZERO);
649 panel.setBorderWidth(0.0);
650 panel.setFillColor(fill);
651 panel
652}
653
654fn text_area_size(text_view: &NSTextView, lines: u32) -> Size {
657 let font = system_font(TextStyle::Body);
658 let line_height = unsafe { text_view.layoutManager() }
660 .expect("a scrollable text view has a layout manager")
661 .defaultLineHeightForFont(&font);
662 let content = NSSize::new(
663 f64::from(MIN_TEXT_FIELD_WIDTH),
664 f64::from(lines) * line_height + 2.0 * TEXT_AREA_INSET.height,
665 );
666 let mtm = MainThreadMarker::from(text_view);
667 let frame = unsafe {
669 NSScrollView::frameSizeForContentSize_horizontalScrollerClass_verticalScrollerClass_borderType_controlSize_scrollerStyle(
670 content,
671 None,
672 None,
673 NSBorderType::BezelBorder,
674 NSControlSize::Regular,
675 NSScroller::preferredScrollerStyle(mtm),
676 mtm,
677 )
678 };
679 to_size(frame)
680}
681
682fn show_title_beside(button: &NSButton, title: &NSString) {
686 button.setTitle(ns_string!(""));
687 button.setToolTip(Some(title));
688 button.setAccessibilityLabel(Some(title));
689}
690
691fn border_on_hover(button: &NSButton) {
693 button.setBezelStyle(NSBezelStyle::AccessoryBar);
694 button.setShowsBorderOnlyWhileMouseInside(true);
695}
696
697pub(crate) fn click_button(
699 mtm: MainThreadMarker,
700 title: &str,
701 target: &ClickTarget,
702) -> Retained<NSButton> {
703 unsafe {
706 NSButton::buttonWithTitle_target_action(
707 &NSString::from_str(title),
708 Some(target),
709 Some(ClickTarget::action()),
710 mtm,
711 )
712 }
713}
714
715pub(crate) fn release_after_event<T: Message>(object: Retained<T>) {
717 let _ = Retained::autorelease_ptr(object);
718}
719
720pub(crate) fn system_font(style: TextStyle) -> Retained<NSFont> {
721 let text_style = unsafe {
723 match style {
724 TextStyle::Body => NSFontTextStyleBody,
725 TextStyle::Title => NSFontTextStyleTitle1,
726 TextStyle::Headline => NSFontTextStyleHeadline,
727 TextStyle::Caption => NSFontTextStyleCaption1,
728 TextStyle::LargeTitle => NSFontTextStyleLargeTitle,
729 }
730 };
731 unsafe { NSFont::preferredFontForTextStyle_options(text_style, &NSDictionary::new()) }
733}
734
735fn text_color(color: TextColor) -> Retained<NSColor> {
736 match color {
737 TextColor::Primary => NSColor::labelColor(),
738 TextColor::Secondary => NSColor::secondaryLabelColor(),
739 TextColor::Tertiary => NSColor::tertiaryLabelColor(),
740 TextColor::Accent => NSColor::controlAccentColor(),
741 TextColor::Success => NSColor::systemGreenColor(),
742 TextColor::Warning => NSColor::systemOrangeColor(),
743 TextColor::Danger => NSColor::systemRedColor(),
744 }
745}
746
747fn text_alignment(align: TextAlign, direction: NSUserInterfaceLayoutDirection) -> NSTextAlignment {
749 match align {
750 TextAlign::Start => NSTextAlignment::Natural,
751 TextAlign::Center => NSTextAlignment::Center,
752 TextAlign::End if direction == NSUserInterfaceLayoutDirection::RightToLeft => {
753 NSTextAlignment::Left
754 }
755 TextAlign::End => NSTextAlignment::Right,
756 }
757}