use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use core_graphics::base::CGFloat;
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use core_graphics::image::CGImage;
use core_graphics::sys::CGContextRef;
use core_text::font::CTFont;
use dispatch2::{DispatchQueue, DispatchTime, MainThreadBound};
use objc2::rc::Retained;
use objc2_app_kit::{
NSCursor, NSEvent, NSFloatingWindowLevel, NSNormalWindowLevel, NSWindow, NSWindowButton,
NSWindowStyleMask,
};
use objc2_foundation::{MainThreadMarker, NSPoint, NSRect, NSSize, NSString};
use crate::backend::{
Backend, BackendError, EditorPaintResult, PointerShape, ResizeEdge, ServiceResult,
WindowControl,
};
use crate::desktop::WindowDragArm;
use crate::dispatch::{DoubleClickDetector, DragState, TextRegion};
use crate::event::{Point, Rect, UiEvent, UserPayload, Viewport};
use crate::generic_font::GenericFamily;
use crate::modal_stack::ModalStack;
use crate::paint_surface::PaintSurface;
use crate::primitives::activity_bar::ActivityBarRowHit;
use crate::primitives::board::{BoardLayout, BoardModel};
use crate::primitives::canvas::{Canvas, CanvasLayout};
use crate::primitives::chart::{Chart, ChartLayout};
use crate::primitives::command_center::{CommandCenter, CommandCenterLayout};
use crate::primitives::command_line::CommandLine;
use crate::primitives::completions::{Completions, CompletionsLayout};
use crate::primitives::context_menu::{ContextMenu, ContextMenuLayout};
use crate::primitives::data_table::{DataTable, DataTableLayout};
use crate::primitives::dialog::{Dialog, DialogInput, DialogLayout};
use crate::primitives::editor::Editor;
use crate::primitives::find_replace::FindReplacePanel;
use crate::primitives::form::FormLayout;
use crate::primitives::menu_bar::{MenuBar, MenuBarLayout};
use crate::primitives::message_list::MessageList;
use crate::primitives::multi_section_view::{
MsvLayoutMetrics, MultiSectionView, MultiSectionViewLayout,
};
use crate::primitives::panel::{Panel, PanelLayout};
use crate::primitives::progress::{ProgressBar, ProgressBarLayout};
use crate::primitives::rich_text_popup::{RichTextPopup, RichTextPopupLayout};
use crate::primitives::scrollbar::Scrollbar;
use crate::primitives::spinner::{Spinner, SpinnerLayout};
use crate::primitives::split::{Split, SplitLayout};
use crate::primitives::split_tree::{SplitTree, SplitTreeLayout};
use crate::primitives::status_bar::StatusBarLayout;
#[allow(deprecated)]
use crate::primitives::tab_bar::TabBarHits;
use crate::primitives::text_display::TextDisplayLayout;
use crate::primitives::toast::{ToastOverlay, ToastStackLayout};
use crate::primitives::tooltip::{Tooltip, TooltipLayout};
use crate::primitives::tree::TreeViewLayout;
use crate::testing::{TextRun, ZoneRec};
use crate::types::{Color, WidgetId};
use crate::KeyBinding;
use crate::{
Accelerator, AcceleratorId, ActivityBar, FieldKind, Form, Key, ListView, Modifiers, Palette,
ParsedBinding, PlatformServices, StatusBar, TabBar, TabBarLayout, Terminal, TextDisplay, Theme,
TreeView,
};
use super::services::MacPlatformServices;
pub struct MacBackend {
viewport: Viewport,
modal_stack: Rc<RefCell<ModalStack>>,
drag_state: Rc<RefCell<DragState>>,
core: crate::backend_core::BackendCore,
double_click: DoubleClickDetector,
events: Rc<std::cell::RefCell<VecDeque<UiEvent>>>,
services: MacPlatformServices,
current_cg_ptr: Cell<*const ()>,
current_theme: Theme,
current_style: crate::Style,
image_cache: crate::image_cache::ImageCache<CGImage>,
minimap_atlas_cache: crate::primitives::minimap::MinimapAtlasCache,
current_font: Option<CTFont>,
current_line_height: f64,
current_char_width: f64,
editor_v_scrollbar_w: Option<f32>,
chrome_font: CTFont,
chrome_line_height: f64,
chrome_char_width: f64,
nerd_font_fallback_family: Option<String>,
menu_target: Option<objc2::rc::Retained<super::menu_bar_install::QuadraMenuTarget>>,
last_native_menu_bar: Option<MenuBar>,
tray: super::tray::MacTrayState,
caret_visible: std::rc::Rc<std::cell::Cell<bool>>,
caret_blink_pause_until: std::rc::Rc<std::cell::Cell<std::time::Instant>>,
zones: Vec<ZoneRec>,
painted_text_recording: bool,
text_runs: Vec<TextRun>,
focused_activity_bar: Option<WidgetId>,
minimap_scale: crate::primitives::minimap::MinimapScale,
window: Option<Retained<NSWindow>>,
pending_window_press: WindowDragArm<Retained<NSEvent>>,
nerd_fonts_enabled: bool,
focus: crate::focus::FocusManager,
user_events: Arc<crate::runtime::UserEventQueue>,
wake_callback: WakeCallback,
tick_callback: WakeCallback,
}
type WakeCallback = Arc<std::sync::OnceLock<MainThreadBound<Rc<dyn Fn()>>>>;
const MAC_DOUBLE_CLICK_RADIUS: f32 = 4.0;
const DEFAULT_UI_FONT_SIZE_PT: f64 = 11.0;
enum ParsedFamily {
Generic(GenericFamily),
Named(String),
}
fn parse_ui_font_desc(desc: &str) -> Option<(ParsedFamily, f64)> {
let desc = desc.trim();
if desc.is_empty() {
return None;
}
let (family_part, size_pt) = match desc.rfind(' ') {
Some(idx) if desc[idx + 1..].trim().parse::<f64>().is_ok() => {
let (family, size_str) = desc.split_at(idx);
(
family.trim(),
size_str
.trim()
.parse::<f64>()
.expect("just matched by the guard above"),
)
}
_ => (desc, DEFAULT_UI_FONT_SIZE_PT),
};
if family_part.is_empty() {
return None;
}
for candidate in family_part.split(',') {
let candidate = candidate.trim();
if candidate.is_empty() {
continue;
}
if let Some(generic) = GenericFamily::parse(candidate) {
return Some((ParsedFamily::Generic(generic), size_pt));
}
if super::text::make_font_exact(candidate, size_pt).is_some() {
return Some((ParsedFamily::Named(candidate.to_string()), size_pt));
}
}
let first = family_part
.split(',')
.map(str::trim)
.find(|c| !c.is_empty())?;
Some((ParsedFamily::Named(first.to_string()), size_pt))
}
fn macos_universal_binding_modifiers(
binding: &KeyBinding,
mut parsed: ParsedBinding,
) -> ParsedBinding {
if !matches!(binding, KeyBinding::Literal(_)) && parsed.modifiers.ctrl {
parsed.modifiers.ctrl = false;
parsed.modifiers.cmd = true;
}
parsed
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MacCursorKind {
Arrow,
ResizeUpDown,
ResizeLeftRight,
}
fn union_ns_rect(a: NSRect, b: NSRect) -> NSRect {
let min_x = a.origin.x.min(b.origin.x);
let min_y = a.origin.y.min(b.origin.y);
let max_x = (a.origin.x + a.size.width).max(b.origin.x + b.size.width);
let max_y = (a.origin.y + a.size.height).max(b.origin.y + b.size.height);
NSRect {
origin: NSPoint { x: min_x, y: min_y },
size: NSSize {
width: max_x - min_x,
height: max_y - min_y,
},
}
}
fn mac_cursor_kind(shape: PointerShape) -> MacCursorKind {
match shape {
PointerShape::Default => MacCursorKind::Arrow,
PointerShape::Resize(ResizeEdge::North | ResizeEdge::South) => MacCursorKind::ResizeUpDown,
PointerShape::Resize(ResizeEdge::East | ResizeEdge::West) => MacCursorKind::ResizeLeftRight,
PointerShape::Resize(
ResizeEdge::NorthEast
| ResizeEdge::NorthWest
| ResizeEdge::SouthEast
| ResizeEdge::SouthWest,
) => MacCursorKind::Arrow,
}
}
fn mac_cursor_for_shape(shape: PointerShape) -> Retained<NSCursor> {
#[allow(deprecated)]
match mac_cursor_kind(shape) {
MacCursorKind::Arrow => NSCursor::arrowCursor(),
MacCursorKind::ResizeUpDown => NSCursor::resizeUpDownCursor(),
MacCursorKind::ResizeLeftRight => NSCursor::resizeLeftRightCursor(),
}
}
impl MacBackend {
pub fn new() -> Self {
super::text::ensure_codicon_registered();
let chrome_font = super::text::font_with_builtin_fallback(
&super::text::system_ui_font(DEFAULT_UI_FONT_SIZE_PT),
None,
);
let chrome_metrics = super::text::font_metrics(&chrome_font);
Self {
viewport: Viewport::new(0.0, 0.0, 1.0),
modal_stack: Rc::new(RefCell::new(ModalStack::new())),
drag_state: Rc::new(RefCell::new(DragState::new())),
core: crate::backend_core::BackendCore::default(),
double_click: DoubleClickDetector::with_radius(MAC_DOUBLE_CLICK_RADIUS),
events: Rc::new(std::cell::RefCell::new(VecDeque::new())),
services: MacPlatformServices::new(),
current_cg_ptr: Cell::new(std::ptr::null()),
current_theme: Theme::default(),
current_style: crate::Style::default(),
image_cache: crate::image_cache::ImageCache::default(),
minimap_atlas_cache: crate::primitives::minimap::MinimapAtlasCache::default(),
current_font: None,
current_line_height: 16.0,
current_char_width: 8.0,
editor_v_scrollbar_w: None,
chrome_font,
chrome_line_height: chrome_metrics.line_height,
chrome_char_width: chrome_metrics.char_width,
nerd_font_fallback_family: None,
menu_target: None,
last_native_menu_bar: None,
tray: super::tray::MacTrayState::default(),
caret_visible: std::rc::Rc::new(std::cell::Cell::new(true)),
caret_blink_pause_until: std::rc::Rc::new(std::cell::Cell::new(
std::time::Instant::now(),
)),
zones: Vec::new(),
painted_text_recording: false,
text_runs: Vec::new(),
focused_activity_bar: None,
minimap_scale: crate::primitives::minimap::MinimapScale::default(),
window: None,
pending_window_press: WindowDragArm::new(),
nerd_fonts_enabled: true,
focus: crate::focus::FocusManager::new(),
user_events: crate::runtime::UserEventQueue::new(),
wake_callback: Arc::new(std::sync::OnceLock::new()),
tick_callback: Arc::new(std::sync::OnceLock::new()),
}
}
pub(crate) fn set_window(&mut self, window: Retained<NSWindow>) {
self.window = Some(window);
}
pub(crate) fn set_wake_callback(&self, callback: Rc<dyn Fn()>, mtm: MainThreadMarker) {
let _ = self.wake_callback.set(MainThreadBound::new(callback, mtm));
}
pub(crate) fn set_tick_callback(&self, callback: Rc<dyn Fn()>, mtm: MainThreadMarker) {
let _ = self.tick_callback.set(MainThreadBound::new(callback, mtm));
}
pub(crate) fn stash_window_press(&mut self, event: Retained<NSEvent>) {
self.pending_window_press.arm(event, 0.0, 0.0);
}
pub fn caret_visible_handle(&self) -> std::rc::Rc<std::cell::Cell<bool>> {
self.caret_visible.clone()
}
pub fn caret_blink_pause_handle(&self) -> std::rc::Rc<std::cell::Cell<std::time::Instant>> {
self.caret_blink_pause_until.clone()
}
pub fn set_caret_visible(&mut self, visible: bool) {
self.caret_visible.set(visible);
}
pub fn caret_visible(&self) -> bool {
self.caret_visible.get()
}
pub fn set_current_font(&mut self, font: CTFont) {
let font = super::text::font_with_builtin_fallback(
&font,
self.nerd_font_fallback_family.as_deref(),
);
let metrics = super::text::font_metrics(&font);
self.current_line_height = metrics.line_height;
self.current_char_width = metrics.char_width;
self.current_font = Some(font);
}
pub fn set_chrome_font(&mut self, font: CTFont) {
let font = super::text::font_with_builtin_fallback(
&font,
self.nerd_font_fallback_family.as_deref(),
);
let metrics = super::text::font_metrics(&font);
self.chrome_line_height = metrics.line_height;
self.chrome_char_width = metrics.char_width;
self.chrome_font = font;
}
pub fn set_current_line_height(&mut self, line_height: f64) {
self.current_line_height = line_height;
}
pub fn set_current_char_width(&mut self, char_width: f64) {
self.current_char_width = char_width;
}
pub fn set_current_theme(&mut self, theme: Theme) {
self.current_theme = theme;
}
pub fn current_theme(&self) -> &Theme {
&self.current_theme
}
pub fn set_current_style(&mut self, style: crate::Style) {
self.current_style = style;
}
pub fn current_style(&self) -> &crate::Style {
&self.current_style
}
pub fn events_handle(&self) -> Rc<std::cell::RefCell<VecDeque<UiEvent>>> {
self.events.clone()
}
pub fn push_event(&self, ev: UiEvent) {
self.events.borrow_mut().push_back(ev);
}
pub fn enter_frame_scope<R>(&mut self, ctx: CGContextRef, f: impl FnOnce(&mut Self) -> R) -> R {
let prev = self.current_cg_ptr.replace(ctx as *const ());
if self.painted_text_recording {
super::text::start_recording_text();
}
let result = f(self);
if self.painted_text_recording {
self.text_runs = super::text::stop_recording_text();
}
self.current_cg_ptr.set(prev);
result
}
pub(crate) fn set_painted_text_recording(&mut self, enabled: bool) {
self.painted_text_recording = enabled;
}
pub(crate) fn text_runs(&self) -> &[TextRun] {
&self.text_runs
}
pub(crate) fn focused_activity_bar_id(&self) -> Option<&WidgetId> {
self.focused_activity_bar.as_ref()
}
pub(crate) fn zones(&self) -> &[ZoneRec] {
&self.zones
}
pub(crate) fn current_cg(&self) -> CGContextRef {
self.current_cg_ptr.get() as CGContextRef
}
pub(crate) fn match_keypress(&self, key: &Key, modifiers: Modifiers) -> Option<AcceleratorId> {
self.core.match_keypress(key, modifiers)
}
pub(crate) fn fold_double_click(&mut self, ev: UiEvent) -> UiEvent {
let mut events = [ev];
self.double_click.process(&mut events);
let [ev] = events;
ev
}
#[cfg(test)]
pub(crate) fn text_regions(&self) -> &[TextRegion] {
&self.core.text_selection.text_regions
}
pub(crate) fn text_selection_state_mut(
&mut self,
) -> &mut crate::text_selection::TextSelectionState {
&mut self.core.text_selection
}
pub(crate) fn active_text_selection(
&self,
) -> Option<&crate::text_selection::ActiveTextSelection> {
self.core.active_text_selection()
}
pub(crate) fn set_active_text_selection(
&mut self,
region: WidgetId,
anchor: crate::event::Point,
focus: crate::event::Point,
) {
self.core.set_active_text_selection(region, anchor, focus);
}
pub(crate) fn clear_selection_display(&mut self) {
self.core.clear_selection_display();
}
pub(crate) fn clear_text_selection(&mut self) {
let mut drag = self.drag_state.borrow_mut();
self.core.clear_text_selection(&mut drag);
}
fn cancel_text_selection_drag_impl(&mut self) {
let mut drag = self.drag_state.borrow_mut();
self.core.cancel_text_selection_drag(&mut drag);
}
pub(crate) fn select_all_text_region(&mut self) -> bool {
self.core.select_all_text_region()
}
pub(crate) fn extract_selection_text(&self) -> String {
self.core.extract_selection_text_pixel(
self.current_line_height as f32,
self.current_char_width as f32,
)
}
pub(crate) fn apply_selection_highlight(&self) {
let char_w = self.current_char_width as f32;
let line_h = self.current_line_height as f32;
let Some(rects) = self.core.selection_highlight_rects(line_h, char_w) else {
return;
};
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::apply_selection_highlight called outside enter_frame_scope",
);
let color = self.current_theme.text_selection_highlight();
unsafe {
super::text_selection::draw_selection_highlight(ctx, &rects, color);
}
}
fn status_bar_paint_scaled(
&mut self,
rect: Rect,
bar: &StatusBar,
interaction: &crate::interaction::InteractionState,
font_scale: f32,
) -> StatusBarLayout {
let (hovered_id, pressed_id) = (interaction.hovered(), interaction.pressed());
let saved = if font_scale != 1.0 {
let scaled_size = (self.chrome_font.pt_size() * font_scale as f64).max(0.1);
let scaled_font = self.chrome_font.clone_with_font_size(scaled_size);
let metrics = super::text::font_metrics(&scaled_font);
let saved_font = std::mem::replace(&mut self.chrome_font, scaled_font);
let saved_lh = self.chrome_line_height;
let saved_cw = self.chrome_char_width;
self.chrome_line_height = metrics.line_height;
self.chrome_char_width = metrics.char_width;
Some((saved_font, saved_lh, saved_cw))
} else {
None
};
let theme = self.current_theme;
let line_height = rect.height;
let mut surface = ChromeSurface { backend: self };
let layout = crate::primitives::status_bar::native_surface_paint::paint(
bar,
&mut surface,
&theme,
rect.x,
rect.y,
rect.width,
line_height,
hovered_id,
pressed_id,
);
if let Some((saved_font, saved_lh, saved_cw)) = saved {
self.chrome_font = saved_font;
self.chrome_line_height = saved_lh;
self.chrome_char_width = saved_cw;
}
layout
}
fn install_menu_bar_if_changed(&mut self, bar: &MenuBar) -> bool {
if self.last_native_menu_bar.as_ref() == Some(bar) {
return false;
}
self.install_menu_bar(bar);
self.last_native_menu_bar = Some(bar.clone());
true
}
fn draw_menu_bar_painted(&mut self, rect: Rect, bar: &MenuBar) -> MenuBarLayout {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_menu_bar called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
unsafe {
super::menu_bar::draw_menu_bar(
ctx,
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
bar,
&theme,
)
}
}
}
fn native_menu_bar_layout(rect: Rect) -> MenuBarLayout {
MenuBarLayout {
bounds: Rect::new(rect.x, rect.y, rect.width, 0.0),
visible_items: Vec::new(),
hit_regions: Vec::new(),
}
}
fn clamp_editor_rect_to_viewport(editor: &Editor, viewport: Viewport) -> Option<Editor> {
if viewport.width <= 0.0 || viewport.height <= 0.0 {
return None;
}
let rect = editor.rect;
let max_width = (viewport.width - rect.x).max(0.0);
let max_height = (viewport.height - rect.y).max(0.0);
if rect.width <= max_width && rect.height <= max_height {
return None;
}
let mut clamped = editor.clone();
clamped.rect.width = rect.width.min(max_width);
clamped.rect.height = rect.height.min(max_height);
Some(clamped)
}
impl Default for MacBackend {
fn default() -> Self {
Self::new()
}
}
impl crate::runtime::PreprocessBackend for MacBackend {
fn active_text_selection(&self) -> Option<&crate::text_selection::ActiveTextSelection> {
self.active_text_selection()
}
fn set_active_text_selection(&mut self, region: WidgetId, anchor: Point, focus: Point) {
self.set_active_text_selection(region, anchor, focus)
}
fn clear_text_selection(&mut self) {
self.clear_text_selection()
}
fn clear_selection_display(&mut self) {
self.clear_selection_display()
}
fn select_all_text_region(&mut self) -> bool {
self.select_all_text_region()
}
fn selection_text_for_copy(&self) -> String {
self.extract_selection_text()
}
fn focused_activity_bar_id(&self) -> Option<&WidgetId> {
self.focused_activity_bar_id()
}
fn focus_manager_mut(&mut self) -> &mut crate::focus::FocusManager {
&mut self.focus
}
fn match_keypress(&self, key: &Key, modifiers: Modifiers) -> Option<AcceleratorId> {
self.match_keypress(key, modifiers)
}
fn fold_double_click(&mut self, ev: UiEvent) -> UiEvent {
self.fold_double_click(ev)
}
fn paste_modifier(&self) -> crate::desktop::PasteModifier {
crate::desktop::PasteModifier::Cmd
}
}
impl crate::backend::sealed::Sealed for MacBackend {}
impl Backend for MacBackend {
fn viewport(&self) -> Viewport {
self.viewport
}
fn begin_frame(&mut self, viewport: Viewport) {
self.viewport = viewport;
self.zones.clear();
self.focused_activity_bar = None;
self.modal_stack.borrow_mut().reset_frame_paint();
self.core.begin_frame();
}
fn end_frame(&mut self) {
#[cfg(debug_assertions)]
for id in self.modal_stack.borrow().unpainted_ids() {
crate::diagnostics::emit(crate::modal_stack::ModalStack::unpainted_modal_message(&id));
}
}
fn set_theme(&mut self, theme: Theme) {
self.set_current_theme(theme);
}
fn theme(&self) -> Theme {
self.current_theme
}
fn set_style(&mut self, style: crate::Style) {
self.set_current_style(style);
}
fn style(&self) -> crate::Style {
self.current_style
}
fn set_nerd_fonts(&mut self, enabled: bool) {
self.nerd_fonts_enabled = enabled;
}
fn nerd_fonts_enabled(&self) -> bool {
self.nerd_fonts_enabled
}
fn register_font_from_memory(&mut self, bytes: &[u8]) -> Option<Vec<String>> {
super::text::register_font_from_memory(bytes).map(|name| vec![name])
}
fn has_font_family(&self, family: &str) -> Option<bool> {
Some(super::text::make_font_exact(family, 12.0).is_some())
}
fn set_nerd_font_fallback(&mut self, family: &str) {
self.nerd_font_fallback_family = Some(family.to_string());
if let Some(font) = self.current_font.take() {
self.current_font = Some(super::text::font_with_builtin_fallback(&font, Some(family)));
}
self.chrome_font = super::text::font_with_builtin_fallback(&self.chrome_font, Some(family));
}
fn set_editor_font(&mut self, family: &str, size_pt: f32) {
let font = match GenericFamily::parse(family) {
Some(GenericFamily::Monospace) => {
Some(super::text::system_monospace_font(size_pt as f64))
}
Some(GenericFamily::SansSerif | GenericFamily::SystemUi) => {
Some(super::text::system_ui_font(size_pt as f64))
}
None => super::text::make_font_exact(family, size_pt as f64),
};
if let Some(font) = font {
self.set_current_font(font);
}
}
fn set_ui_font(&mut self, font_desc: &str) {
let font = match parse_ui_font_desc(font_desc) {
Some((ParsedFamily::Generic(GenericFamily::Monospace), size_pt)) => {
super::text::system_monospace_font(size_pt)
}
Some((
ParsedFamily::Generic(GenericFamily::SansSerif | GenericFamily::SystemUi),
size_pt,
)) => super::text::system_ui_font(size_pt),
Some((ParsedFamily::Named(family), size_pt)) => {
super::text::make_font_exact(&family, size_pt)
.unwrap_or_else(|| super::text::system_ui_font(size_pt))
}
None => super::text::system_ui_font(DEFAULT_UI_FONT_SIZE_PT),
};
self.set_chrome_font(font);
}
fn default_fonts(&self) -> crate::backend::PlatformFontDefaults {
crate::backend::PlatformFontDefaults {
editor_family: "Menlo".to_string(),
editor_size_pt: 12.0,
ui_family: super::text::system_ui_font(13.0).family_name(),
ui_size_pt: 13.0,
}
}
fn poll_events(&mut self) -> Vec<UiEvent> {
let mut out: Vec<UiEvent> = self.events.borrow_mut().drain(..).collect();
self.user_events.drain_into(&mut out);
out
}
fn wait_events(&mut self, _timeout: Duration) -> Vec<UiEvent> {
self.poll_events()
}
fn waker(&self) -> Arc<dyn Fn(UserPayload) + Send + Sync> {
let queue = Arc::clone(&self.user_events);
let wake_callback = Arc::clone(&self.wake_callback);
Arc::new(move |payload: UserPayload| {
queue.push(payload.into_arc());
let wake_callback = Arc::clone(&wake_callback);
DispatchQueue::main().exec_async(move || {
if let Some(bound) = wake_callback.get() {
let mtm = unsafe { MainThreadMarker::new_unchecked() };
(bound.get(mtm))();
}
});
})
}
fn request_frame_in(&self, delay: Duration) {
let tick_callback = Arc::clone(&self.tick_callback);
let Ok(when) = DispatchTime::try_from(delay) else {
return;
};
let _ = DispatchQueue::main().after(when, move || {
if let Some(bound) = tick_callback.get() {
let mtm = unsafe { MainThreadMarker::new_unchecked() };
(bound.get(mtm))();
}
});
}
fn register_accelerator(&mut self, acc: &Accelerator) {
let binding = acc.binding.clone();
self.core.register_accelerator_with(acc, |parsed| {
macos_universal_binding_modifiers(&binding, parsed)
});
}
fn unregister_accelerator(&mut self, id: &AcceleratorId) {
self.core.unregister_accelerator(id);
}
fn menu_style(&self) -> crate::backend::MenuStyle {
self.core.menu_style()
}
fn set_menu_style(&mut self, style: crate::backend::MenuStyle) {
self.core.set_menu_style(style);
}
fn install_menu_bar(&mut self, bar: &crate::primitives::menu_bar::MenuBar) {
let Some(mtm) = objc2_foundation::MainThreadMarker::new() else {
return;
};
let target = super::menu_bar_install::install_menu_bar(mtm, bar, self.events.clone());
self.menu_target = Some(target);
}
fn show_context_menu(
&mut self,
menu: &crate::primitives::context_menu::ContextMenu,
anchor: crate::event::Point,
) {
let Some(mtm) = objc2_foundation::MainThreadMarker::new() else {
return;
};
super::menu_bar_install::show_context_menu(
mtm,
menu,
anchor.x as f64,
anchor.y as f64,
self.events.clone(),
);
}
fn modal_stack_handle(&self) -> Rc<RefCell<ModalStack>> {
self.modal_stack.clone()
}
fn drag_state_handle(&self) -> Rc<RefCell<DragState>> {
self.drag_state.clone()
}
fn focus_manager(&self) -> &crate::focus::FocusManager {
&self.focus
}
fn draw_focus_ring(&mut self, rect: Rect) {
let theme = self.current_theme;
let stroke_width = self.current_style.focus_ring_width;
self.surface_stroke_rect(rect, theme.accent_fg, stroke_width);
self.register_zone(WidgetId::new("chrome:focus-ring"), rect);
}
fn services(&self) -> &dyn PlatformServices {
&self.services
}
fn paint_surface(&mut self) -> Option<&mut dyn PaintSurface> {
Some(self)
}
fn register_zone(&mut self, id: WidgetId, bounds: Rect) {
self.zones.push(ZoneRec { id, bounds });
}
fn register_text_region(&mut self, region: TextRegion) {
self.core.register_text_region(region);
}
fn cancel_text_selection_drag(&mut self) {
self.cancel_text_selection_drag_impl();
}
fn backend_caps(&self) -> crate::backend::BackendCaps {
crate::backend::BackendCaps {
mouse: true,
scroll: true,
drag: true,
native_menu: true,
file_dialogs: true,
folder_dialogs: true,
notifications: true,
window_chrome: true,
pointer_cursor: true,
text_selection: true,
app_font_registration: true,
native_dialogs: true,
generic_font_families: true,
window_control: true,
tray: true,
..crate::backend::BackendCaps::empty()
}
}
fn begin_window_drag(&mut self) -> bool {
let Some(window) = self.window.as_ref() else {
return false;
};
let Some(event) = self.pending_window_press.take() else {
return false;
};
window.performWindowDragWithEvent(&event);
true
}
fn toggle_window_maximize(&mut self) -> bool {
let Some(window) = self.window.as_ref() else {
return false;
};
window.zoom(None);
true
}
fn set_cursor(&mut self, shape: PointerShape) -> bool {
if self.window.is_none() {
return false;
}
mac_cursor_for_shape(shape).set();
true
}
fn window(&mut self) -> Option<&mut dyn crate::backend::WindowControl> {
self.window.as_ref()?;
Some(self)
}
fn tray(&mut self) -> Option<&mut dyn crate::backend::TrayService> {
Some(self)
}
fn titlebar_control_inset(&self) -> Rect {
let Some(window) = self.window.as_ref() else {
return Rect::default();
};
let button_kinds = [
NSWindowButton::CloseButton,
NSWindowButton::MiniaturizeButton,
NSWindowButton::ZoomButton,
];
let mut union_rect: Option<NSRect> = None;
for kind in button_kinds {
let Some(button) = window.standardWindowButton(kind) else {
continue;
};
if button.isHiddenOrHasHiddenAncestor() {
continue;
}
let Some(superview) = (unsafe { button.superview() }) else {
continue;
};
let in_window = superview.convertRect_toView(button.frame(), None);
union_rect = Some(match union_rect {
Some(acc) => union_ns_rect(acc, in_window),
None => in_window,
});
}
let Some(in_window) = union_rect else {
return Rect::default();
};
let content_height = window
.contentView()
.map(|view| view.frame().size.height)
.unwrap_or_else(|| window.frame().size.height);
let bottom_from_top = (content_height - in_window.origin.y).max(0.0);
Rect::new(
0.0,
0.0,
(in_window.origin.x + in_window.size.width) as f32,
bottom_from_top as f32,
)
}
fn line_height(&self) -> f32 {
self.current_line_height as f32
}
fn chrome_line_height(&self) -> f32 {
self.chrome_line_height as f32
}
fn char_width(&self) -> f32 {
self.current_char_width as f32
}
fn set_current_line_height(&mut self, line_height: f32) {
MacBackend::set_current_line_height(self, line_height as f64);
}
fn set_current_char_width(&mut self, char_width: f32) {
MacBackend::set_current_char_width(self, char_width as f64);
}
fn list_char_width(&self) -> f32 {
self.chrome_char_width as f32
}
fn measure_text(&self, text: &str, role: crate::FontRole) -> (f32, f32) {
let font = match role {
crate::FontRole::Chrome => &self.chrome_font,
crate::FontRole::Editor => self.current_font.as_ref().unwrap_or(&self.chrome_font),
};
let (w, h) = super::text::measure_text(font, text);
(w as f32, h as f32)
}
fn draw_tree(&mut self, rect: Rect, tree: &TreeView) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_tree called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let line_height = self.chrome_line_height;
unsafe {
super::tree::draw_tree(
ctx,
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
tree,
&theme,
line_height,
self.nerd_fonts_enabled,
);
}
}
fn draw_list(&mut self, rect: Rect, list: &ListView) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_list called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let line_height = self.chrome_line_height;
unsafe {
super::list::draw_list(
ctx,
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
list,
&theme,
line_height,
self.nerd_fonts_enabled,
);
}
}
fn draw_data_table(
&mut self,
rect: Rect,
table: &DataTable,
hovered_idx: Option<usize>,
) -> DataTableLayout {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_data_table called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let line_height = self.current_line_height;
unsafe {
super::data_table::draw_data_table(
ctx,
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
table,
&theme,
line_height,
hovered_idx,
)
}
}
fn data_table_layout(&self, rect: Rect, table: &DataTable) -> DataTableLayout {
let font = &self.chrome_font;
super::data_table::mac_data_table_layout(
table,
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
self.current_line_height,
)
}
fn list_hscrollbar(&self, rect: Rect, list: &ListView) -> Option<crate::Scrollbar> {
let char_w = self.current_char_width as f32;
let max_w_chars = list.max_content_width? as f32;
let content_px = max_w_chars * char_w;
let border_inset = if list.bordered { char_w } else { 0.0 };
let visible_px = (rect.width - 2.0 * border_inset).max(0.0);
if content_px <= visible_px {
return None;
}
let row_h = self.line_height();
let track = crate::primitives::list::hscrollbar_track(rect, list.bordered, char_w, row_h);
Some(crate::Scrollbar::horizontal(
list.id.clone(),
track,
list.h_scroll as f32 * char_w,
content_px,
visible_px,
row_h,
))
}
fn list_vscrollbar(&self, rect: Rect, list: &ListView) -> Option<crate::Scrollbar> {
let row_h = self.chrome_line_height();
list.vscrollbar(
crate::event::Rect::new(rect.x, rect.y, rect.width, rect.height),
row_h,
)
}
fn list_layout(&self, rect: Rect, list: &ListView) -> crate::ListViewLayout {
super::list::mac_list_layout(
list,
rect.width as f64,
rect.height as f64,
self.chrome_line_height,
self.chrome_char_width,
)
}
fn draw_form(&mut self, rect: Rect, form: &Form) {
debug_assert!(
!self.current_cg().is_null(),
"MacBackend::draw_form called outside enter_frame_scope",
);
let font = self.chrome_font.clone();
let theme = self.current_theme;
let flayout = super::form::mac_form_layout(form, rect, self.current_line_height, &font);
let origin = Point::new(rect.x, rect.y);
crate::primitives::form::paint(form, &flayout, self, &theme, origin);
for vf in &flayout.visible_fields {
let Some(field) = form.fields.get(vf.field_idx) else {
continue;
};
let FieldKind::Toolbar(toolbar) = &field.kind else {
continue;
};
let label_text: String = field.label.spans.iter().map(|s| s.text.as_str()).collect();
let no_label = label_text.is_empty();
let (label_w, _) = super::text::measure_text(&font, &label_text);
let row_x = (origin.x + vf.bounds.x) as f64;
let row_y = (origin.y + vf.bounds.y) as f64;
let row_w = vf.bounds.width as f64;
let row_h = vf.bounds.height as f64;
let toolbar_x = if no_label {
row_x + 6.0
} else {
row_x + 6.0 + label_w + 12.0
};
let toolbar_w = row_x + row_w - toolbar_x;
if toolbar_w > 0.0 {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_form called outside enter_frame_scope",
);
unsafe {
super::toolbar::draw_toolbar(
ctx, &font, toolbar_x, row_y, toolbar_w, row_h, toolbar, &theme, None, None,
);
}
}
}
}
fn draw_palette(&mut self, rect: Rect, palette: &Palette) {
self.modal_stack.borrow_mut().mark_painted(&palette.id);
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_palette called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let style = self.style();
let line_height = self.current_line_height;
if rect.width < 20.0 || rect.height < line_height as f32 * 4.0 {
return;
}
let (palette_layout, rows_h) = crate::primitives::palette::native_surface_paint::layout(
rect.width,
rect.height,
palette,
line_height as f32,
);
let mut surface = super::surface::CgSurface {
ctx,
font: Some(font),
};
crate::primitives::palette::native_surface_paint::paint(
palette,
rect,
&palette_layout,
rows_h,
line_height as f32,
self.nerd_fonts_enabled,
&mut surface,
&theme,
&style,
);
}
fn palette_layout(&self, rect: Rect, palette: &Palette) -> crate::PaletteLayout {
super::palette::mac_palette_layout(
palette,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
self.current_line_height,
)
}
fn draw_settings_chrome(
&mut self,
rect: Rect,
header_text: &str,
query: &str,
placeholder: &str,
active: bool,
) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_settings_chrome called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let line_height = self.current_line_height;
unsafe {
super::form::draw_settings_chrome(
ctx,
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
line_height,
header_text,
query,
placeholder,
active,
&theme,
);
}
}
fn draw_status_bar_interactive(
&mut self,
rect: Rect,
bar: &StatusBar,
interaction: &crate::interaction::InteractionState,
) -> StatusBarLayout {
self.status_bar_paint_scaled(rect, bar, interaction, 1.0)
}
fn draw_status_bar_interactive_scaled(
&mut self,
rect: Rect,
bar: &StatusBar,
interaction: &crate::interaction::InteractionState,
font_scale: f32,
) -> StatusBarLayout {
self.status_bar_paint_scaled(rect, bar, interaction, font_scale)
}
#[allow(deprecated)] fn draw_tab_bar(
&mut self,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
) -> TabBarHits {
self.draw_tab_bar_icons(rect, bar, &[], hovered_close_tab)
}
#[allow(deprecated)] fn draw_tab_bar_icons(
&mut self,
rect: Rect,
bar: &TabBar,
icons: &[Option<crate::TabIcon>],
hovered_close_tab: Option<usize>,
) -> TabBarHits {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_tab_bar called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let line_height = self.chrome_line_height;
unsafe {
CGContextSaveGState(ctx);
CGContextTranslateCTM(ctx, rect.x as f64, 0.0);
let mut hits = super::tab_bar::draw_tab_bar_icons(
ctx,
font,
rect.width as f64,
line_height,
rect.y as f64,
rect.height as f64,
bar,
&theme,
hovered_close_tab,
icons,
);
CGContextRestoreGState(ctx);
crate::backend::shift_tab_bar_hits(&mut hits, rect.x as f64);
hits
}
}
fn draw_tab_bar_layout(
&mut self,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
) -> TabBarLayout {
self.draw_tab_bar_icons_layout(rect, bar, &[], hovered_close_tab)
}
fn draw_tab_bar_icons_layout(
&mut self,
rect: Rect,
bar: &TabBar,
icons: &[Option<crate::TabIcon>],
hovered_close_tab: Option<usize>,
) -> TabBarLayout {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_tab_bar_layout called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let line_height = self.chrome_line_height;
#[allow(deprecated)] unsafe {
CGContextSaveGState(ctx);
CGContextTranslateCTM(ctx, rect.x as f64, 0.0);
let _ = super::tab_bar::draw_tab_bar_icons(
ctx,
font,
rect.width as f64,
line_height,
rect.y as f64,
rect.height as f64,
bar,
&theme,
hovered_close_tab,
icons,
);
CGContextRestoreGState(ctx);
}
super::tab_bar::mac_tab_bar_native_layout_icons(
font,
rect.width as f64,
rect.height as f64,
bar,
icons,
)
}
fn draw_activity_bar(
&mut self,
rect: Rect,
bar: &ActivityBar,
hovered_idx: Option<usize>,
) -> Vec<ActivityBarRowHit> {
if bar.is_keyboard_focused {
self.focused_activity_bar = Some(bar.id.clone());
}
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_activity_bar called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
unsafe {
CGContextSaveGState(ctx);
CGContextTranslateCTM(ctx, rect.x as f64, rect.y as f64);
let hits = super::activity_bar::draw_activity_bar(
ctx,
font,
rect.width as f64,
rect.height as f64,
bar,
&theme,
hovered_idx,
self.nerd_fonts_enabled,
);
CGContextRestoreGState(ctx);
hits
}
}
fn draw_activity_bar_with_style(
&mut self,
rect: Rect,
bar: &ActivityBar,
hovered_idx: Option<usize>,
style: &crate::ActivityBarStyle,
) -> Vec<ActivityBarRowHit> {
if bar.is_keyboard_focused {
self.focused_activity_bar = Some(bar.id.clone());
}
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_activity_bar_with_style called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
unsafe {
CGContextSaveGState(ctx);
CGContextTranslateCTM(ctx, rect.x as f64, rect.y as f64);
let hits = super::activity_bar::draw_activity_bar_with_style(
ctx,
font,
rect.width as f64,
rect.height as f64,
bar,
style,
&theme,
hovered_idx,
self.nerd_fonts_enabled,
);
CGContextRestoreGState(ctx);
hits
}
}
fn status_bar_layout(&self, rect: Rect, bar: &StatusBar) -> StatusBarLayout {
super::status_bar::mac_status_bar_layout(
&self.chrome_font,
rect.width as f64,
self.chrome_line_height,
bar,
)
}
#[allow(deprecated)] fn tab_bar_layout(&self, rect: Rect, bar: &TabBar) -> TabBarHits {
self.tab_bar_layout_icons(rect, bar, &[])
}
#[allow(deprecated)] fn tab_bar_layout_icons(
&self,
rect: Rect,
bar: &TabBar,
icons: &[Option<crate::TabIcon>],
) -> TabBarHits {
let mut hits = super::tab_bar::mac_tab_bar_layout_icons(
&self.chrome_font,
rect.width as f64,
bar,
icons,
);
crate::backend::shift_tab_bar_hits(&mut hits, rect.x as f64);
hits
}
fn resolve_tab_bar_layout(&self, rect: Rect, bar: &TabBar) -> TabBarLayout {
self.resolve_tab_bar_layout_icons(rect, bar, &[])
}
fn resolve_tab_bar_layout_icons(
&self,
rect: Rect,
bar: &TabBar,
icons: &[Option<crate::TabIcon>],
) -> TabBarLayout {
super::tab_bar::mac_tab_bar_native_layout_icons(
&self.chrome_font,
rect.width as f64,
rect.height as f64,
bar,
icons,
)
}
fn activity_bar_layout(&self, rect: Rect, bar: &ActivityBar) -> Vec<ActivityBarRowHit> {
super::activity_bar::mac_activity_bar_layout(rect.width as f64, rect.height as f64, bar)
}
fn draw_terminal(&mut self, rect: Rect, term: &Terminal) {
let theme = self.current_theme;
let line_height = self.current_line_height;
let char_width = self.current_char_width;
let sb_width = match &term.scrollbar {
Some(sb) => sb.width.map(|w| w as f64).unwrap_or(8.0),
None => 0.0,
};
let cell_area_w = (rect.width as f64 - sb_width).max(0.0);
{
let mut surface = EditorSurface { backend: self };
crate::primitives::terminal::paint(
term,
&mut surface,
&theme,
rect.x,
rect.y,
cell_area_w as f32,
rect.height,
line_height as f32,
char_width as f32,
None,
);
}
if let Some(ref sb_state) = term.scrollbar {
let sb = crate::primitives::scrollbar::Scrollbar::vertical(
term.id.clone(),
Rect::new(
rect.x + cell_area_w as f32,
rect.y,
sb_width as f32,
rect.height,
),
sb_state.effective_scroll_offset() as f32,
sb_state.total_lines as f32,
sb_state.visible_lines as f32,
line_height as f32,
);
crate::primitives::scrollbar::native_surface_paint::paint(&sb, self, &theme);
}
}
fn draw_terminal_divider(&mut self, rect: Rect) {
let theme = self.current_theme;
crate::primitives::terminal::paint_divider(self, rect.x, rect.y, rect.height, &theme);
}
fn draw_solid_fill(&mut self, rect: Rect, color: Color) {
self.surface_fill_rect(rect, color);
self.register_zone(WidgetId::new("chrome:solid-fill"), rect);
}
fn draw_text_display(&mut self, rect: Rect, td: &TextDisplay) {
let theme = self.current_theme;
let line_height = self.current_line_height as f32;
let char_width = self.current_char_width as f32;
let mut surface = EditorSurface { backend: self };
crate::primitives::text_display::paint(
td,
rect,
&mut surface,
&theme,
line_height,
char_width,
);
}
fn draw_command_line(&mut self, rect: Rect, cmd: &CommandLine) {
self.draw_command_line_selection(rect, cmd, None);
}
fn draw_command_line_selection(
&mut self,
rect: Rect,
cmd: &CommandLine,
selection: Option<(usize, usize)>,
) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_command_line_selection called outside enter_frame_scope",
);
let font = self.current_font.as_ref().unwrap_or(&self.chrome_font);
let theme = self.current_theme;
let line_height = self.current_line_height;
let char_width = self.current_char_width as f32;
unsafe {
super::command_line::draw_command_line_selection(
ctx,
font,
cmd,
&theme,
rect.x as f64,
rect.y as f64,
rect.width as f64,
line_height,
char_width,
selection,
);
}
}
fn command_line_layout(
&self,
rect: Rect,
cmd: &CommandLine,
) -> crate::primitives::command_line::CommandLineLayout {
cmd.layout(
rect,
crate::primitives::command_line::CommandLineMeasure::from_metrics(&self.measure()),
)
}
fn text_display_layout(&self, rect: Rect, td: &TextDisplay) -> TextDisplayLayout {
super::text_display::mac_text_display_layout(
td,
rect,
self.current_line_height,
self.current_char_width,
)
}
fn draw_text_input(
&mut self,
rect: Rect,
ti: &crate::primitives::text_input::TextInput,
) -> crate::primitives::text_input::TextInputLayout {
debug_assert!(
!self.current_cg().is_null(),
"MacBackend::draw_text_input called outside enter_frame_scope",
);
let theme = self.current_theme;
let line_height = self.chrome_line_height as f32;
let char_width = self.chrome_char_width as f32;
let layout = super::text_input::mac_text_input_layout(ti, rect, line_height, char_width);
crate::primitives::text_input::paint(ti, &layout, self, &theme);
layout
}
fn text_input_layout(
&self,
rect: Rect,
ti: &crate::primitives::text_input::TextInput,
) -> crate::primitives::text_input::TextInputLayout {
super::text_input::mac_text_input_layout(
ti,
rect,
self.chrome_line_height as f32,
self.chrome_char_width as f32,
)
}
fn draw_tooltip(&mut self, tooltip: &Tooltip, layout: &TooltipLayout) {
self.draw_tooltip_with_chrome(tooltip, layout, &crate::TooltipChrome::default());
}
fn draw_tooltip_with_chrome(
&mut self,
tooltip: &Tooltip,
layout: &TooltipLayout,
chrome: &crate::TooltipChrome,
) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_tooltip called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let style = self.style();
let line_height = self.current_line_height;
let char_width = self.current_char_width;
let mut surface = super::surface::CgSurface {
ctx,
font: Some(font),
};
crate::primitives::tooltip::native_surface_paint::paint(
tooltip,
layout,
chrome,
line_height as f32,
char_width as f32,
&mut surface,
&theme,
&style,
);
}
fn draw_context_menu(
&mut self,
menu: &ContextMenu,
layout: &ContextMenuLayout,
) -> Vec<(Rect, WidgetId)> {
self.modal_stack.borrow_mut().mark_painted(&menu.id);
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_context_menu called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let style = self.style();
let mut surface = super::surface::CgSurface {
ctx,
font: Some(font),
};
crate::primitives::context_menu::native_surface_paint::paint(
menu,
layout,
crate::accelerator::Platform::Macos,
&mut surface,
&theme,
&style,
)
}
fn draw_dialog(&mut self, dialog: &Dialog, layout: &DialogLayout) -> Vec<Rect> {
self.modal_stack.borrow_mut().mark_painted(&dialog.id);
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_dialog called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let style = self.style();
let line_height = self.current_line_height;
let mut surface = super::surface::CgSurface {
ctx,
font: Some(font),
};
let button_rects = crate::primitives::dialog::native_surface_paint::paint(
dialog,
layout,
line_height as f32,
&mut surface,
&theme,
&style,
);
if let (Some(input_b), Some(DialogInput::Toolbar(toolbar))) =
(layout.input_bounds, dialog.input.as_ref())
{
unsafe {
super::toolbar::draw_toolbar(
ctx,
font,
input_b.x as f64,
input_b.y as f64,
input_b.width as f64,
input_b.height as f64,
toolbar,
&theme,
None,
None,
);
}
}
button_rects
}
fn draw_float(
&mut self,
float: &crate::primitives::float::Float,
layout: &crate::primitives::float::FloatLayout,
) {
self.modal_stack.borrow_mut().mark_painted(&float.id);
let theme = self.current_theme;
crate::primitives::float::native_surface_paint::paint(float, layout, self, &theme);
self.register_zone(float.id.clone(), layout.bounds);
}
fn draw_number_input(
&mut self,
number_input: &crate::primitives::number_input::NumberInput,
layout: &crate::primitives::number_input::NumberInputLayout,
) {
let theme = self.current_theme;
crate::primitives::number_input::native_surface_paint::paint(
number_input,
layout,
self,
&theme,
);
self.register_zone(number_input.id.clone(), layout.bounds);
}
fn draw_combo_box(
&mut self,
combo_box: &crate::primitives::combo_box::ComboBox,
layout: &crate::primitives::combo_box::ComboBoxLayout,
) {
let theme = self.current_theme;
crate::primitives::combo_box::native_surface_paint::paint(combo_box, layout, self, &theme);
self.register_zone(combo_box.id.clone(), layout.field_bounds);
}
fn draw_multi_section_view(&mut self, rect: Rect, view: &MultiSectionView) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_multi_section_view called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let line_height = self.chrome_line_height;
let char_width = self.chrome_char_width;
let caret_visible = self.caret_visible.get();
unsafe {
super::multi_section_view::draw_multi_section_view(
ctx,
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
view,
&theme,
line_height,
char_width,
caret_visible,
)
}
}
fn msv_layout(&self, rect: Rect, view: &MultiSectionView) -> MultiSectionViewLayout {
super::multi_section_view::mac_msv_layout(view, rect, self.chrome_line_height)
}
fn msv_metrics(&self) -> MsvLayoutMetrics {
super::multi_section_view::mac_msv_metrics(self.chrome_line_height, false)
}
fn tree_layout(&self, rect: Rect, tree: &TreeView) -> TreeViewLayout {
super::tree::mac_tree_layout(tree, rect, self.chrome_line_height)
}
fn tree_vscrollbar(&self, rect: Rect, tree: &TreeView) -> Option<crate::Scrollbar> {
let row_h =
crate::primitives::layout_metrics::tree_row_pitch(tree, self.chrome_line_height);
tree.vscrollbar_with_track_width(
rect,
row_h as f32,
crate::primitives::layout_metrics::pixel::TREE_VSCROLLBAR_WIDTH,
)
}
fn tree_paints_own_scrollbar(&self) -> bool {
true
}
fn form_layout(&self, rect: Rect, form: &Form) -> FormLayout {
let font = &self.chrome_font;
super::form::mac_form_layout(form, rect, self.current_line_height, font)
}
fn draw_editor(&mut self, _rect: Rect, editor: &Editor) -> EditorPaintResult {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_editor called outside enter_frame_scope",
);
let font = self.current_font.as_ref().unwrap_or(&self.chrome_font);
let theme = self.current_theme;
let line_height = self.current_line_height;
let char_width = self.current_char_width;
let v_scrollbar_w = self.editor_v_scrollbar_w;
let clamped = clamp_editor_rect_to_viewport(editor, self.viewport);
let editor = clamped.as_ref().unwrap_or(editor);
unsafe {
super::editor::draw_editor_with_options_and_v_scrollbar_w(
ctx,
font,
editor,
&theme,
char_width,
line_height,
crate::primitives::editor::EditorPaintOptions::default(),
v_scrollbar_w,
)
}
}
fn set_editor_v_scrollbar_width(&mut self, px: Option<f32>) {
self.editor_v_scrollbar_w = px;
}
fn editor_v_scrollbar_width(&self) -> Option<f32> {
self.editor_v_scrollbar_w
}
fn draw_message_list(&mut self, rect: Rect, list: &MessageList) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_message_list called outside enter_frame_scope",
);
let font = &self.chrome_font;
let line_height = self.current_line_height;
unsafe {
super::message_list::draw_message_list(
ctx,
font,
list,
rect.x as f64,
rect.y as f64,
rect.width as f64,
(rect.y + rect.height) as f64,
line_height,
);
}
}
fn draw_rich_text_popup(&mut self, popup: &RichTextPopup, layout: &RichTextPopupLayout) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_rich_text_popup called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
unsafe { super::rich_text_popup::draw_rich_text_popup(ctx, font, popup, layout, &theme) }
}
fn draw_rich_text_popup_with_font_role(
&mut self,
popup: &RichTextPopup,
layout: &RichTextPopupLayout,
role: crate::FontRole,
) {
if role == crate::FontRole::Chrome {
self.draw_rich_text_popup(popup, layout);
return;
}
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_rich_text_popup_with_font_role called outside enter_frame_scope",
);
let font = self.current_font.as_ref().unwrap_or(&self.chrome_font);
let theme = self.current_theme;
unsafe { super::rich_text_popup::draw_rich_text_popup(ctx, font, popup, layout, &theme) }
}
fn draw_find_replace(&mut self, _rect: Rect, panel: &FindReplacePanel) {
debug_assert!(
!self.current_cg().is_null(),
"MacBackend::draw_find_replace called outside enter_frame_scope",
);
let theme = self.current_theme;
crate::primitives::find_replace::paint(panel, self, &theme);
}
fn draw_completions(&mut self, completions: &Completions, layout: &CompletionsLayout) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_completions called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
unsafe { super::completions::draw_completions(ctx, font, completions, layout, &theme) }
}
fn draw_scrollbar(&mut self, _rect: Rect, scrollbar: &Scrollbar) {
let theme = self.current_theme;
crate::primitives::scrollbar::native_surface_paint::paint(scrollbar, self, &theme);
}
fn draw_drop_overlay(&mut self, overlay: &crate::primitives::drop_zone::DropOverlay) {
let theme = self.current_theme;
crate::primitives::drop_zone::native_surface_paint::paint(overlay, self, &theme);
}
fn draw_menu_bar(&mut self, rect: Rect, bar: &MenuBar) -> MenuBarLayout {
match self.effective_menu_style() {
crate::backend::ResolvedMenuStyle::Native => {
self.install_menu_bar_if_changed(bar);
native_menu_bar_layout(rect)
}
crate::backend::ResolvedMenuStyle::Custom => self.draw_menu_bar_painted(rect, bar),
}
}
fn menu_bar_layout(&self, rect: Rect, bar: &MenuBar) -> MenuBarLayout {
if self.effective_menu_style() == crate::backend::ResolvedMenuStyle::Native {
return native_menu_bar_layout(rect);
}
let font = &self.chrome_font;
super::menu_bar::mac_menu_bar_layout(
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
bar,
)
}
fn draw_split(&mut self, rect: Rect, split: &Split) -> SplitLayout {
let layout = super::split::mac_split_layout(
split,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
);
let theme = self.current_theme;
crate::primitives::split::native_surface_paint::paint(&layout, self, &theme);
layout
}
fn split_layout(&self, rect: Rect, split: &Split) -> SplitLayout {
super::split::mac_split_layout(
split,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
)
}
fn draw_split_tree(&mut self, rect: Rect, tree: &SplitTree) -> SplitTreeLayout {
let theme = self.current_theme;
let layout = super::split_tree::mac_split_tree_layout(
tree,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
);
crate::primitives::split_tree::native_surface_paint::paint(&layout, self, &theme);
layout
}
fn split_tree_layout(&self, rect: Rect, tree: &SplitTree) -> SplitTreeLayout {
super::split_tree::mac_split_tree_layout(
tree,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
)
}
fn draw_panel(&mut self, rect: Rect, panel: &Panel) -> PanelLayout {
let theme = self.current_theme;
let line_height = self.current_line_height;
let layout = super::panel::mac_panel_layout(
panel,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
line_height,
);
crate::primitives::panel::native_surface_paint::paint(panel, &layout, self, &theme);
layout
}
fn panel_layout(&self, rect: Rect, panel: &Panel) -> PanelLayout {
super::panel::mac_panel_layout(
panel,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
self.current_line_height,
)
}
fn draw_canvas(&mut self, rect: Rect, canvas: &Canvas) -> CanvasLayout {
let layout = canvas.layout(rect);
crate::primitives::canvas::native_surface_paint::paint(canvas, &layout, self);
layout
}
fn draw_toast_overlay(&mut self, rect: Rect, stack: &ToastOverlay) -> ToastStackLayout {
let theme = self.current_theme;
let style = self.style();
let line_height = self.current_line_height as f32;
crate::primitives::toast::native_surface_paint::paint(
stack,
self,
&theme,
&style,
rect.x,
rect.y,
rect.width,
rect.height,
line_height,
)
}
fn toast_stack_layout(&self, rect: Rect, stack: &ToastOverlay) -> ToastStackLayout {
let font = &self.chrome_font;
super::toast::mac_toast_stack_layout(
stack,
font,
rect.x,
rect.y,
rect.width,
rect.height,
self.current_line_height,
)
}
fn draw_pipeline_view(
&mut self,
rect: Rect,
view: &crate::primitives::pipeline_view::PipelineView,
) -> crate::primitives::pipeline_view::PipelineViewLayout {
let theme = self.current_theme;
crate::primitives::pipeline_view::native_surface_paint::paint(view, self, &theme, rect)
}
fn pipeline_view_layout(
&self,
rect: Rect,
view: &crate::primitives::pipeline_view::PipelineView,
) -> crate::primitives::pipeline_view::PipelineViewLayout {
super::pipeline_view::mac_pipeline_view_layout(
view,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
)
}
fn draw_progress(&mut self, rect: Rect, bar: &ProgressBar) -> ProgressBarLayout {
let theme = self.current_theme;
let style = self.style();
crate::primitives::progress::native_surface_paint::paint(bar, self, &theme, rect, &style)
}
fn progress_layout(&self, rect: Rect, bar: &ProgressBar) -> ProgressBarLayout {
super::progress::mac_progress_layout(
bar,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
)
}
fn draw_spinner(&mut self, rect: Rect, spinner: &Spinner) -> SpinnerLayout {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_spinner called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
unsafe {
super::spinner::draw_spinner(ctx, font, rect.x as f64, rect.y as f64, spinner, &theme)
}
}
fn spinner_layout(&self, rect: Rect, spinner: &Spinner) -> SpinnerLayout {
let font = &self.chrome_font;
super::spinner::mac_spinner_layout(spinner, font, rect.x as f64, rect.y as f64)
}
fn draw_command_center(&mut self, rect: Rect, cc: &CommandCenter) -> CommandCenterLayout {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_command_center called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
let line_height = self.current_line_height;
unsafe {
super::command_center::draw_command_center(
ctx,
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
cc,
&theme,
line_height,
)
}
}
fn command_center_layout(&self, rect: Rect, cc: &CommandCenter) -> CommandCenterLayout {
let font = &self.chrome_font;
super::command_center::mac_command_center_layout(
cc,
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
)
}
fn draw_chart(
&mut self,
rect: Rect,
chart: &Chart,
hovered_point: Option<(usize, usize)>,
crosshair_x: Option<f64>,
) -> ChartLayout {
let theme = self.current_theme;
let layout = self.chart_layout(rect, chart);
crate::primitives::chart::paint(chart, &layout, self, &theme, hovered_point, crosshair_x);
layout
}
fn chart_layout(&self, rect: Rect, chart: &Chart) -> ChartLayout {
super::chart::mac_chart_layout(
chart,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
self.chrome_line_height,
self.chrome_char_width,
)
}
fn draw_toolbar_with_options(
&mut self,
rect: Rect,
bar: &crate::primitives::toolbar::Toolbar,
interaction: &crate::interaction::InteractionState,
options: crate::primitives::toolbar::ToolbarPaintOptions,
) -> crate::primitives::toolbar::ToolbarLayout {
let (hovered_id, pressed_id) = (interaction.hovered(), interaction.pressed());
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_toolbar called outside enter_frame_scope",
);
let font = &self.chrome_font;
let theme = self.current_theme;
unsafe {
super::toolbar::draw_toolbar_with_options(
ctx,
font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
bar,
&theme,
hovered_id,
pressed_id,
options,
)
}
}
fn toolbar_layout(
&self,
rect: Rect,
bar: &crate::primitives::toolbar::Toolbar,
) -> crate::primitives::toolbar::ToolbarLayout {
super::toolbar::mac_toolbar_layout(
bar,
&self.chrome_font,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
)
}
fn draw_sidebar_panel_interactive(
&mut self,
rect: Rect,
panel: &crate::primitives::sidebar_panel::SidebarPanel,
interaction: &crate::interaction::InteractionState,
) -> crate::primitives::sidebar_panel::SidebarPanelLayout {
let (hovered_toolbar_id, pressed_toolbar_id) =
(interaction.hovered(), interaction.pressed());
debug_assert!(
!self.current_cg().is_null(),
"MacBackend::draw_sidebar_panel called outside enter_frame_scope",
);
let theme = self.current_theme;
let line_height = self.chrome_line_height as f32;
let mut surface = ChromeSurface { backend: self };
crate::primitives::sidebar_panel::native_surface_paint::paint(
panel,
&mut surface,
&theme,
rect,
line_height,
hovered_toolbar_id,
pressed_toolbar_id,
)
}
fn draw_diff_view(
&mut self,
rect: Rect,
view: &crate::primitives::diff_view::DiffView,
) -> crate::primitives::diff_view::DiffViewLayout {
let theme = self.current_theme;
let line_height = self.current_line_height as f32;
let mut surface = EditorSurface { backend: self };
crate::primitives::diff_view::native_surface_paint::paint(
view,
&mut surface,
&theme,
rect,
line_height,
)
}
fn sidebar_panel_layout(
&self,
rect: Rect,
panel: &crate::primitives::sidebar_panel::SidebarPanel,
) -> crate::primitives::sidebar_panel::SidebarPanelLayout {
super::sidebar_panel::mac_sidebar_panel_layout(
panel,
&self.chrome_font,
self.chrome_line_height,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
)
}
fn draw_board(&mut self, rect: Rect, model: &BoardModel) -> BoardLayout {
let theme = self.current_theme;
crate::primitives::board::native_surface_paint::paint(model, self, &theme, rect)
}
fn board_layout(&self, rect: Rect, model: &BoardModel) -> BoardLayout {
super::board::mac_board_layout(
model,
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
)
}
fn draw_minimap(
&mut self,
rect: Rect,
minimap: &crate::primitives::minimap::Minimap,
) -> crate::backend::MinimapPaintResult {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::draw_minimap called outside enter_frame_scope",
);
self.register_zone(minimap.id.clone(), rect);
let font = self.current_font.as_ref().unwrap_or(&self.chrome_font);
let theme = self.current_theme;
let dpi_scale = self.viewport.scale as f64;
let layout = unsafe {
super::minimap::draw_minimap_cached(
ctx,
font,
rect,
minimap,
&theme,
self.minimap_scale,
&mut self.minimap_atlas_cache,
dpi_scale,
)
};
crate::backend::MinimapPaintResult {
layout,
painted: true,
}
}
fn minimap_layout(
&self,
rect: Rect,
minimap: &crate::primitives::minimap::Minimap,
) -> crate::primitives::minimap::MinimapLayout {
super::minimap::mac_minimap_layout_scaled(minimap, rect, self.minimap_scale)
}
fn minimap_scale(&self) -> crate::primitives::minimap::MinimapScale {
self.minimap_scale
}
fn set_minimap_scale(&mut self, scale: crate::primitives::minimap::MinimapScale) {
self.minimap_scale = scale;
}
fn draw_image(
&mut self,
rect: Rect,
image: &crate::primitives::image::Image,
) -> crate::backend::ImagePaintResult {
self.register_zone(image.id.clone(), rect);
let ctx = self.current_cg();
let scale = self.viewport.scale;
unsafe { super::image::draw_image_cached(ctx, rect, image, &mut self.image_cache, scale) }
}
}
impl WindowControl for MacBackend {
fn set_title(&mut self, title: &str) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.setTitle(&NSString::from_str(title));
Ok(())
}
fn set_size(&mut self, width: f32, height: f32) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.setContentSize(NSSize {
width: width as f64,
height: height as f64,
});
Ok(())
}
fn set_min_size(&mut self, width: f32, height: f32) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.setContentMinSize(NSSize {
width: width as f64,
height: height as f64,
});
Ok(())
}
fn set_max_size(&mut self, width: f32, height: f32) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.setContentMaxSize(NSSize {
width: width as f64,
height: height as f64,
});
Ok(())
}
fn bounds(&self) -> ServiceResult<Rect> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
let frame = window.frame();
Ok(Rect::new(
frame.origin.x as f32,
frame.origin.y as f32,
frame.size.width as f32,
frame.size.height as f32,
))
}
fn set_bounds(&mut self, bounds: Rect) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.setFrame_display(
NSRect {
origin: NSPoint {
x: bounds.x as f64,
y: bounds.y as f64,
},
size: NSSize {
width: bounds.width as f64,
height: bounds.height as f64,
},
},
true,
);
Ok(())
}
fn center(&mut self) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.center();
Ok(())
}
fn set_fullscreen(&mut self, fullscreen: bool) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
let currently_fullscreen = window.styleMask().contains(NSWindowStyleMask::FullScreen);
if currently_fullscreen != fullscreen {
window.toggleFullScreen(None);
}
Ok(())
}
fn is_maximized(&self) -> ServiceResult<bool> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
Ok(window.isZoomed())
}
fn set_always_on_top(&mut self, on_top: bool) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.setLevel(if on_top {
NSFloatingWindowLevel
} else {
NSNormalWindowLevel
});
Ok(())
}
fn set_decorated(&mut self, decorated: bool) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
let mut style = window.styleMask();
if decorated {
style.insert(NSWindowStyleMask::Titled);
} else {
style.remove(NSWindowStyleMask::Titled);
}
window.setStyleMask(style);
Ok(())
}
fn minimize(&mut self) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.miniaturize(None);
Ok(())
}
fn restore(&mut self) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
if window.isMiniaturized() {
window.deminiaturize(None);
}
if window.isZoomed() {
window.zoom(None);
}
Ok(())
}
fn hide(&mut self) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.orderOut(None);
Ok(())
}
fn show(&mut self) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.makeKeyAndOrderFront(None);
Ok(())
}
fn focus(&mut self) -> ServiceResult<()> {
let window = self.window.as_ref().ok_or(BackendError::Unsupported)?;
window.makeKeyAndOrderFront(None);
Ok(())
}
}
impl crate::backend::TrayService for MacBackend {
fn set_icon(&mut self, icon: crate::primitives::image::ImageSource) -> ServiceResult<()> {
super::tray::set_icon(&mut self.tray, self.events.clone(), icon)
}
fn set_tooltip(&mut self, tooltip: &str) -> ServiceResult<()> {
super::tray::set_tooltip(&mut self.tray, self.events.clone(), tooltip)
}
fn set_menu(
&mut self,
menu: &crate::primitives::context_menu::ContextMenu,
) -> ServiceResult<()> {
super::tray::set_menu(&mut self.tray, self.events.clone(), menu)
}
}
struct ChromeSurface<'a> {
backend: &'a mut MacBackend,
}
impl PaintSurface for ChromeSurface<'_> {
fn surface_begin_frame(&mut self, viewport: Viewport) {
self.backend.surface_begin_frame(viewport)
}
fn surface_end_frame(&mut self) {
self.backend.surface_end_frame()
}
fn surface_viewport(&self) -> Viewport {
self.backend.surface_viewport()
}
fn surface_line_height(&self) -> f32 {
self.backend.chrome_line_height as f32
}
fn surface_char_width(&self) -> f32 {
self.backend.chrome_char_width as f32
}
fn surface_measure_text(&self, text: &str) -> (f32, f32) {
let (w, h) = super::text::measure_text(&self.backend.chrome_font, text);
(w as f32, h as f32)
}
fn surface_fill_rect(&mut self, rect: Rect, color: Color) {
self.backend.surface_fill_rect(rect, color)
}
fn surface_fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
self.backend.surface_fill_rounded_rect(rect, radius, color)
}
fn surface_stroke_rect(&mut self, rect: Rect, color: Color, stroke_width: f32) {
self.backend.surface_stroke_rect(rect, color, stroke_width)
}
fn surface_stroke_rounded_rect(
&mut self,
rect: Rect,
radius: f32,
color: Color,
stroke_width: f32,
) {
self.backend
.surface_stroke_rounded_rect(rect, radius, color, stroke_width)
}
fn surface_draw_text_run(&mut self, rect: Rect, text: &str, color: Color) {
let ctx = self.backend.current_cg();
debug_assert!(
!ctx.is_null(),
"ChromeSurface::surface_draw_text_run called outside enter_frame_scope",
);
unsafe {
super::text::draw_text(
ctx,
&self.backend.chrome_font,
text,
rect.x as f64,
rect.y as f64,
ns_color_to_cg(color),
);
}
}
#[allow(clippy::too_many_arguments)]
fn surface_draw_text_run_styled(
&mut self,
rect: Rect,
text: &str,
color: Color,
bold: bool,
italic: bool,
underline: bool,
scale_x: f32,
) {
let _ = (bold, italic, underline);
let ctx = self.backend.current_cg();
debug_assert!(
!ctx.is_null(),
"ChromeSurface::surface_draw_text_run_styled called outside enter_frame_scope",
);
unsafe {
super::text::draw_text_scaled_x(
ctx,
&self.backend.chrome_font,
text,
rect.x as f64,
rect.y as f64,
scale_x as f64,
ns_color_to_cg(color),
);
}
}
fn surface_draw_line(&mut self, from: Point, to: Point, color: Color, stroke_width: f32) {
self.backend
.surface_draw_line(from, to, color, stroke_width)
}
fn surface_push_clip(&mut self, rect: Rect) {
self.backend.surface_push_clip(rect)
}
fn surface_pop_clip(&mut self) {
self.backend.surface_pop_clip()
}
fn surface_draw_image(
&mut self,
rect: Rect,
image: &crate::primitives::image::Image,
) -> crate::backend::ImagePaintResult {
self.backend.surface_draw_image(rect, image)
}
}
struct EditorSurface<'a> {
backend: &'a mut MacBackend,
}
impl PaintSurface for EditorSurface<'_> {
fn surface_begin_frame(&mut self, viewport: Viewport) {
self.backend.surface_begin_frame(viewport)
}
fn surface_end_frame(&mut self) {
self.backend.surface_end_frame()
}
fn surface_viewport(&self) -> Viewport {
self.backend.surface_viewport()
}
fn surface_line_height(&self) -> f32 {
self.backend.current_line_height as f32
}
fn surface_char_width(&self) -> f32 {
self.backend.current_char_width as f32
}
fn surface_measure_text(&self, text: &str) -> (f32, f32) {
let font = self
.backend
.current_font
.as_ref()
.unwrap_or(&self.backend.chrome_font);
let (w, h) = super::text::measure_text(font, text);
(w as f32, h as f32)
}
fn surface_fill_rect(&mut self, rect: Rect, color: Color) {
self.backend.surface_fill_rect(rect, color)
}
fn surface_fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
self.backend.surface_fill_rounded_rect(rect, radius, color)
}
fn surface_stroke_rect(&mut self, rect: Rect, color: Color, stroke_width: f32) {
self.backend.surface_stroke_rect(rect, color, stroke_width)
}
fn surface_stroke_rounded_rect(
&mut self,
rect: Rect,
radius: f32,
color: Color,
stroke_width: f32,
) {
self.backend
.surface_stroke_rounded_rect(rect, radius, color, stroke_width)
}
fn surface_draw_text_run(&mut self, rect: Rect, text: &str, color: Color) {
let ctx = self.backend.current_cg();
debug_assert!(
!ctx.is_null(),
"EditorSurface::surface_draw_text_run called outside enter_frame_scope",
);
let font = self
.backend
.current_font
.as_ref()
.unwrap_or(&self.backend.chrome_font);
unsafe {
super::text::draw_text(
ctx,
font,
text,
rect.x as f64,
rect.y as f64,
ns_color_to_cg(color),
);
}
}
#[allow(clippy::too_many_arguments)]
fn surface_draw_text_run_styled(
&mut self,
rect: Rect,
text: &str,
color: Color,
bold: bool,
italic: bool,
underline: bool,
scale_x: f32,
) {
let _ = (bold, italic, underline);
let ctx = self.backend.current_cg();
debug_assert!(
!ctx.is_null(),
"EditorSurface::surface_draw_text_run_styled called outside enter_frame_scope",
);
let font = self
.backend
.current_font
.as_ref()
.unwrap_or(&self.backend.chrome_font);
unsafe {
super::text::draw_text_scaled_x(
ctx,
font,
text,
rect.x as f64,
rect.y as f64,
scale_x as f64,
ns_color_to_cg(color),
);
}
}
fn surface_draw_line(&mut self, from: Point, to: Point, color: Color, stroke_width: f32) {
self.backend
.surface_draw_line(from, to, color, stroke_width)
}
fn surface_push_clip(&mut self, rect: Rect) {
self.backend.surface_push_clip(rect)
}
fn surface_pop_clip(&mut self) {
self.backend.surface_pop_clip()
}
fn surface_draw_image(
&mut self,
rect: Rect,
image: &crate::primitives::image::Image,
) -> crate::backend::ImagePaintResult {
self.backend.surface_draw_image(rect, image)
}
}
impl PaintSurface for MacBackend {
fn surface_begin_frame(&mut self, viewport: Viewport) {
Backend::begin_frame(self, viewport);
}
fn surface_end_frame(&mut self) {
Backend::end_frame(self);
}
fn surface_viewport(&self) -> Viewport {
Backend::viewport(self)
}
fn surface_line_height(&self) -> f32 {
self.chrome_line_height as f32
}
fn surface_char_width(&self) -> f32 {
self.chrome_char_width as f32
}
fn surface_measure_text(&self, text: &str) -> (f32, f32) {
let (w, h) = super::text::measure_text(&self.chrome_font, text);
(w as f32, h as f32)
}
fn surface_fill_rect(&mut self, rect: Rect, color: Color) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::surface_fill_rect called outside enter_frame_scope",
);
unsafe { ns_fill_rect(ctx, rect, color) };
}
fn surface_fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::surface_fill_rounded_rect called outside enter_frame_scope",
);
unsafe { ns_fill_rounded_rect(ctx, rect, radius, color) };
}
fn surface_stroke_rect(&mut self, rect: Rect, color: Color, stroke_width: f32) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::surface_stroke_rect called outside enter_frame_scope",
);
unsafe { ns_stroke_rect(ctx, rect, color, stroke_width as f64) };
}
fn surface_stroke_rounded_rect(
&mut self,
rect: Rect,
radius: f32,
color: Color,
stroke_width: f32,
) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::surface_stroke_rounded_rect called outside enter_frame_scope",
);
unsafe { ns_stroke_rounded_rect(ctx, rect, radius, color, stroke_width as f64) };
}
fn surface_draw_text_run(&mut self, rect: Rect, text: &str, color: Color) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::surface_draw_text_run called outside enter_frame_scope",
);
unsafe {
super::text::draw_text(
ctx,
&self.chrome_font,
text,
rect.x as f64,
rect.y as f64,
ns_color_to_cg(color),
);
}
}
#[allow(clippy::too_many_arguments)]
fn surface_draw_text_run_styled(
&mut self,
rect: Rect,
text: &str,
color: Color,
bold: bool,
italic: bool,
underline: bool,
scale_x: f32,
) {
let _ = (bold, italic, underline);
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::surface_draw_text_run_styled called outside enter_frame_scope",
);
unsafe {
super::text::draw_text_scaled_x(
ctx,
&self.chrome_font,
text,
rect.x as f64,
rect.y as f64,
scale_x as f64,
ns_color_to_cg(color),
);
}
}
fn surface_draw_text_run_with_role(
&mut self,
rect: Rect,
text: &str,
color: Color,
role: crate::FontRole,
italic: bool,
) {
let _ = italic;
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::surface_draw_text_run_with_role called outside enter_frame_scope",
);
let font = match role {
crate::FontRole::Chrome => &self.chrome_font,
crate::FontRole::Editor => self.current_font.as_ref().unwrap_or(&self.chrome_font),
};
unsafe {
super::text::draw_text(
ctx,
font,
text,
rect.x as f64,
rect.y as f64,
ns_color_to_cg(color),
);
}
}
fn surface_draw_icon_glyph(&mut self, rect: Rect, text: &str, color: Color) {
self.surface_draw_text_run(rect, text, color)
}
fn surface_draw_line(&mut self, from: Point, to: Point, color: Color, stroke_width: f32) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::surface_draw_line called outside enter_frame_scope",
);
unsafe {
ns_draw_line(
ctx,
from.x as f64,
from.y as f64,
to.x as f64,
to.y as f64,
color,
stroke_width as f64,
);
}
}
fn surface_push_clip(&mut self, rect: Rect) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::surface_push_clip called outside enter_frame_scope",
);
unsafe { ns_push_clip(ctx, rect) };
}
fn surface_pop_clip(&mut self) {
let ctx = self.current_cg();
debug_assert!(
!ctx.is_null(),
"MacBackend::surface_pop_clip called outside enter_frame_scope",
);
unsafe { CGContextRestoreGState(ctx) };
}
fn surface_draw_image(
&mut self,
rect: Rect,
image: &crate::primitives::image::Image,
) -> crate::backend::ImagePaintResult {
Backend::draw_image(self, rect, image)
}
}
pub(crate) fn ns_color_to_cg(c: Color) -> (f64, f64, f64, f64) {
(
c.r as f64 / 255.0,
c.g as f64 / 255.0,
c.b as f64 / 255.0,
c.a as f64 / 255.0,
)
}
fn ns_cg_rect(rect: Rect) -> CGRect {
CGRect::new(
&CGPoint::new(rect.x as f64, rect.y as f64),
&CGSize::new(rect.width as f64, rect.height as f64),
)
}
pub(crate) unsafe fn ns_fill_rect(ctx: CGContextRef, rect: Rect, c: Color) {
let (r, g, b, a) = ns_color_to_cg(c);
CGContextSetRGBFillColor(ctx, r, g, b, a);
CGContextFillRect(ctx, ns_cg_rect(rect));
}
pub(crate) unsafe fn ns_fill_rounded_rect(ctx: CGContextRef, rect: Rect, radius: f32, c: Color) {
if rect.width <= 0.0 || rect.height <= 0.0 {
return;
}
let r = (radius as f64)
.min(rect.width as f64 / 2.0)
.min(rect.height as f64 / 2.0)
.max(0.0);
let (x, y, w, h) = (
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
);
let (r0, g0, b0, a0) = ns_color_to_cg(c);
CGContextSetRGBFillColor(ctx, r0, g0, b0, a0);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, x + r, y);
CGContextAddLineToPoint(ctx, x + w - r, y);
CGContextAddArcToPoint(ctx, x + w, y, x + w, y + r, r);
CGContextAddLineToPoint(ctx, x + w, y + h - r);
CGContextAddArcToPoint(ctx, x + w, y + h, x + w - r, y + h, r);
CGContextAddLineToPoint(ctx, x + r, y + h);
CGContextAddArcToPoint(ctx, x, y + h, x, y + h - r, r);
CGContextAddLineToPoint(ctx, x, y + r);
CGContextAddArcToPoint(ctx, x, y, x + r, y, r);
CGContextClosePath(ctx);
CGContextFillPath(ctx);
}
pub(crate) unsafe fn ns_stroke_rect(ctx: CGContextRef, rect: Rect, c: Color, line_width: f64) {
let (r, g, b, a) = ns_color_to_cg(c);
CGContextSetRGBStrokeColor(ctx, r, g, b, a);
CGContextSetLineWidth(ctx, line_width);
CGContextStrokeRect(ctx, ns_cg_rect(rect));
}
pub(crate) unsafe fn ns_stroke_rounded_rect(
ctx: CGContextRef,
rect: Rect,
radius: f32,
c: Color,
line_width: f64,
) {
let r = (radius as f64)
.min(rect.width as f64 / 2.0)
.min(rect.height as f64 / 2.0)
.max(0.0);
let (x, y, w, h) = (
rect.x as f64,
rect.y as f64,
rect.width as f64,
rect.height as f64,
);
let (r0, g0, b0, a0) = ns_color_to_cg(c);
CGContextSetRGBStrokeColor(ctx, r0, g0, b0, a0);
CGContextSetLineWidth(ctx, line_width);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, x + r, y);
CGContextAddLineToPoint(ctx, x + w - r, y);
CGContextAddArcToPoint(ctx, x + w, y, x + w, y + r, r);
CGContextAddLineToPoint(ctx, x + w, y + h - r);
CGContextAddArcToPoint(ctx, x + w, y + h, x + w - r, y + h, r);
CGContextAddLineToPoint(ctx, x + r, y + h);
CGContextAddArcToPoint(ctx, x, y + h, x, y + h - r, r);
CGContextAddLineToPoint(ctx, x, y + r);
CGContextAddArcToPoint(ctx, x, y, x + r, y, r);
CGContextClosePath(ctx);
CGContextStrokePath(ctx);
}
pub(crate) unsafe fn ns_draw_line(
ctx: CGContextRef,
x0: f64,
y0: f64,
x1: f64,
y1: f64,
c: Color,
line_width: f64,
) {
let (r, g, b, a) = ns_color_to_cg(c);
CGContextSetRGBStrokeColor(ctx, r, g, b, a);
CGContextSetLineWidth(ctx, line_width);
CGContextMoveToPoint(ctx, x0, y0);
CGContextAddLineToPoint(ctx, x1, y1);
CGContextStrokePath(ctx);
}
pub(crate) unsafe fn ns_push_clip(ctx: CGContextRef, rect: Rect) {
CGContextSaveGState(ctx);
CGContextClipToRect(ctx, ns_cg_rect(rect));
}
pub(crate) unsafe fn ns_pop_clip(ctx: CGContextRef) {
CGContextRestoreGState(ctx);
}
extern "C" {
fn CGContextSaveGState(c: CGContextRef);
fn CGContextRestoreGState(c: CGContextRef);
fn CGContextTranslateCTM(c: CGContextRef, tx: CGFloat, ty: CGFloat);
fn CGContextClipToRect(c: CGContextRef, rect: CGRect);
fn CGContextSetRGBFillColor(
c: CGContextRef,
red: CGFloat,
green: CGFloat,
blue: CGFloat,
alpha: CGFloat,
);
fn CGContextSetRGBStrokeColor(
c: CGContextRef,
red: CGFloat,
green: CGFloat,
blue: CGFloat,
alpha: CGFloat,
);
fn CGContextSetLineWidth(c: CGContextRef, width: CGFloat);
fn CGContextFillRect(c: CGContextRef, rect: CGRect);
fn CGContextStrokeRect(c: CGContextRef, rect: CGRect);
fn CGContextMoveToPoint(c: CGContextRef, x: CGFloat, y: CGFloat);
fn CGContextAddLineToPoint(c: CGContextRef, x: CGFloat, y: CGFloat);
fn CGContextStrokePath(c: CGContextRef);
fn CGContextBeginPath(c: CGContextRef);
fn CGContextAddArcToPoint(
c: CGContextRef,
x1: CGFloat,
y1: CGFloat,
x2: CGFloat,
y2: CGFloat,
radius: CGFloat,
);
fn CGContextClosePath(c: CGContextRef);
fn CGContextFillPath(c: CGContextRef);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::accelerator::{Accelerator, AcceleratorScope};
use crate::event::Point;
use crate::types::Modifiers;
use crate::KeyBinding;
fn acc(id: &str, key: &str) -> Accelerator {
Accelerator {
id: AcceleratorId::new(id),
binding: KeyBinding::Literal(key.to_string()),
scope: AcceleratorScope::Global,
label: None,
}
}
#[test]
fn new_starts_with_default_viewport() {
let b = MacBackend::new();
let v = b.viewport();
assert_eq!(v.width, 0.0);
assert_eq!(v.height, 0.0);
assert_eq!(v.scale, 1.0);
}
#[test]
fn mac_backend_new_self_registers_the_bundled_codicon_font() {
let _ = MacBackend::new();
assert!(
crate::macos::text::make_font_exact(crate::codicon::FONT_FAMILY, 12.0).is_some(),
"MacBackend::new() must leave the bundled codicon font registered"
);
}
#[test]
fn mac_backend_nerd_fonts_enabled_defaults_to_true() {
let b = MacBackend::new();
assert!(
b.nerd_fonts_enabled(),
"icons are on by default on this GUI backend — the bundled \
codicon font is always available here"
);
}
#[test]
fn begin_frame_updates_viewport() {
let mut b = MacBackend::new();
b.begin_frame(Viewport::new(800.0, 600.0, 2.0));
let v = b.viewport();
assert_eq!(v.width, 800.0);
assert_eq!(v.height, 600.0);
assert_eq!(v.scale, 2.0);
}
#[test]
fn mac_backend_modal_stack_handle_shares_state() {
let backend = MacBackend::new();
let h1 = backend.modal_stack_handle();
let h2 = backend.modal_stack_handle();
h1.borrow_mut().push(
crate::types::WidgetId::new("test:popup"),
crate::event::Rect::new(0.0, 0.0, 10.0, 5.0),
);
assert_eq!(h2.borrow().len(), 1);
}
#[test]
fn mac_backend_auto_resolves_native_and_custom_resolves_painted() {
let mut backend = MacBackend::new();
assert_eq!(backend.menu_style(), crate::backend::MenuStyle::Auto);
assert_eq!(
backend.effective_menu_style(),
crate::backend::ResolvedMenuStyle::Native
);
backend.set_menu_style(crate::backend::MenuStyle::Custom);
assert_eq!(backend.menu_style(), crate::backend::MenuStyle::Custom);
assert_eq!(
backend.effective_menu_style(),
crate::backend::ResolvedMenuStyle::Custom
);
}
#[test]
fn surface_measure_text_always_reads_chrome_font() {
let mut backend = MacBackend::new();
let (w, h) = backend.surface_measure_text("A");
assert!(
w > 0.0 && h > 0.0,
"must measure through the always-`Some` chrome_font, not panic or report zero: \
got ({w}, {h})"
);
let (expect_w, expect_h) = crate::macos::text::measure_text(&backend.chrome_font, "A");
assert_eq!((w, h), (expect_w as f32, expect_h as f32));
backend.set_current_font(
crate::macos::text::make_font("Menlo", 200.0).expect("Menlo installed"),
);
let (w2, h2) = backend.surface_measure_text("A");
assert_eq!(
(w2, h2),
(w, h),
"surface_measure_text must stay pinned to chrome_font even once current_font is set"
);
}
#[test]
fn install_menu_bar_off_main_thread_does_not_panic() {
let bar = crate::primitives::menu_bar::MenuBar {
id: WidgetId::new("menubar"),
items: vec![],
open_item: None,
focused_item: None,
};
let handle = std::thread::spawn(move || {
let mut backend = MacBackend::new();
backend.install_menu_bar(&bar);
backend.menu_target.is_none()
});
let no_target_installed = handle
.join()
.expect("install_menu_bar must return, not panic, off the main thread (#930)");
assert!(
no_target_installed,
"off the main thread there is no MainThreadMarker, so install_menu_bar must be a \
no-op that leaves menu_target unset"
);
}
#[test]
fn show_context_menu_off_main_thread_does_not_panic() {
let menu = crate::primitives::context_menu::ContextMenu {
id: WidgetId::new("ctx"),
items: vec![],
selected_idx: 0,
bg: None,
placement: Default::default(),
};
let handle = std::thread::spawn(move || {
let mut backend = MacBackend::new();
backend.show_context_menu(&menu, Point::new(0.0, 0.0));
});
handle
.join()
.expect("show_context_menu must return, not panic, off the main thread (#930)");
}
#[test]
fn modal_stack_handle_outlives_the_backend_borrow_through_the_trait() {
let mut backend = MacBackend::new();
let stack_rc = {
let dyn_backend: &mut dyn Backend = &mut backend;
dyn_backend.modal_stack_handle()
};
backend.begin_frame(Viewport::new(80.0, 24.0, 1.0));
stack_rc.borrow_mut().push(
crate::types::WidgetId::new("test:popup"),
crate::event::Rect::new(0.0, 0.0, 10.0, 5.0),
);
assert_eq!(backend.modal_stack_handle().borrow().len(), 1);
}
#[test]
fn mac_backend_drag_state_handle_shares_state() {
let backend = MacBackend::new();
let h1 = backend.drag_state_handle();
let h2 = backend.drag_state_handle();
h1.borrow_mut()
.begin(crate::dispatch::DragTarget::TextSelection {
region: crate::types::WidgetId::new("r"),
anchor: Point::new(0.0, 0.0),
});
assert!(h2.borrow().is_active());
}
#[test]
fn services_platform_name_is_macos() {
let b = MacBackend::new();
assert_eq!(b.services().platform_name(), "macos");
}
#[test]
fn line_height_and_char_width_seed_to_defaults() {
let b = MacBackend::new();
assert_eq!(b.line_height(), 16.0);
assert_eq!(b.char_width(), 8.0);
}
#[test]
fn register_and_unregister_accelerator_round_trip() {
let mut b = MacBackend::new();
let a = acc("save", "<C-s>");
b.register_accelerator(&a);
let ctrl_s = Modifiers {
ctrl: true,
..Default::default()
};
assert_eq!(
b.match_keypress(&Key::Char('s'), ctrl_s),
Some(AcceleratorId::new("save"))
);
b.unregister_accelerator(&AcceleratorId::new("save"));
assert_eq!(b.match_keypress(&Key::Char('s'), ctrl_s), None);
}
#[test]
fn match_keypress_finds_registered_global_binding() {
let mut b = MacBackend::new();
b.register_accelerator(&acc("save", "<D-s>"));
let id = b.match_keypress(
&crate::Key::Char('s'),
Modifiers {
cmd: true,
..Default::default()
},
);
assert_eq!(id, Some(AcceleratorId::new("save")));
}
#[test]
fn match_keypress_modifier_mismatch_no_match() {
let mut b = MacBackend::new();
b.register_accelerator(&acc("save", "<D-s>"));
let id = b.match_keypress(
&crate::Key::Char('s'),
Modifiers {
ctrl: true,
..Default::default()
},
);
assert_eq!(id, None);
}
#[test]
fn match_keypress_skips_non_global_scope() {
let mut b = MacBackend::new();
b.register_accelerator(&Accelerator {
id: AcceleratorId::new("find-in-tree"),
binding: KeyBinding::Literal("<D-f>".to_string()),
scope: AcceleratorScope::Mode("tree".into()),
label: None,
});
let id = b.match_keypress(
&crate::Key::Char('f'),
Modifiers {
cmd: true,
..Default::default()
},
);
assert_eq!(id, None, "non-Global scope must not match here");
}
#[test]
fn match_keypress_unregister_removes_match() {
let mut b = MacBackend::new();
b.register_accelerator(&acc("save", "<D-s>"));
b.unregister_accelerator(&AcceleratorId::new("save"));
let id = b.match_keypress(
&crate::Key::Char('s'),
Modifiers {
cmd: true,
..Default::default()
},
);
assert_eq!(id, None);
}
#[test]
fn match_keypress_re_register_replaces_binding() {
let mut b = MacBackend::new();
b.register_accelerator(&acc("save", "<D-s>"));
b.register_accelerator(&acc("save", "<D-S-s>")); assert!(b
.match_keypress(
&crate::Key::Char('s'),
Modifiers {
cmd: true,
..Default::default()
}
)
.is_none());
assert_eq!(
b.match_keypress(
&crate::Key::Char('s'),
Modifiers {
cmd: true,
shift: true,
..Default::default()
}
),
Some(AcceleratorId::new("save"))
);
}
#[test]
fn match_keypress_universal_binding_matches_native_cmd_not_ctrl() {
let mut b = MacBackend::new();
b.register_accelerator(&Accelerator {
id: AcceleratorId::new("save"),
binding: KeyBinding::Save,
scope: AcceleratorScope::Global,
label: None,
});
assert_eq!(
b.match_keypress(
&crate::Key::Char('s'),
Modifiers {
cmd: true,
..Default::default()
},
),
Some(AcceleratorId::new("save")),
);
assert_eq!(
b.match_keypress(
&crate::Key::Char('s'),
Modifiers {
ctrl: true,
..Default::default()
},
),
None,
);
}
#[test]
fn match_keypress_universal_binding_preserves_shift_modifier() {
let mut b = MacBackend::new();
b.register_accelerator(&Accelerator {
id: AcceleratorId::new("redo"),
binding: KeyBinding::Redo,
scope: AcceleratorScope::Global,
label: None,
});
assert_eq!(
b.match_keypress(
&crate::Key::Char('z'),
Modifiers {
cmd: true,
shift: true,
..Default::default()
},
),
Some(AcceleratorId::new("redo")),
);
}
#[test]
fn match_keypress_literal_binding_ctrl_is_not_translated_to_cmd() {
let mut b = MacBackend::new();
b.register_accelerator(&acc("literal-ctrl-s", "<C-s>"));
assert_eq!(
b.match_keypress(
&crate::Key::Char('s'),
Modifiers {
ctrl: true,
..Default::default()
},
),
Some(AcceleratorId::new("literal-ctrl-s")),
);
assert_eq!(
b.match_keypress(
&crate::Key::Char('s'),
Modifiers {
cmd: true,
..Default::default()
},
),
None,
);
}
fn mouse_down(x: f32, y: f32) -> UiEvent {
UiEvent::MouseDown {
widget: None,
button: crate::MouseButton::Left,
position: Point::new(x, y),
modifiers: Modifiers::default(),
}
}
#[test]
fn fold_double_click_second_click_same_position_becomes_double_click() {
let mut b = MacBackend::new();
let first = b.fold_double_click(mouse_down(5.0, 3.0));
assert!(matches!(first, UiEvent::MouseDown { .. }));
let second = b.fold_double_click(mouse_down(5.0, 3.0));
assert!(
matches!(second, UiEvent::DoubleClick { .. }),
"second click at the same position should fold to DoubleClick"
);
}
#[test]
fn fold_double_click_different_position_stays_mouse_down() {
let mut b = MacBackend::new();
let _ = b.fold_double_click(mouse_down(5.0, 3.0));
let second = b.fold_double_click(mouse_down(50.0, 30.0));
assert!(matches!(second, UiEvent::MouseDown { .. }));
}
#[test]
fn fold_double_click_passes_non_mouse_down_events_through() {
let mut b = MacBackend::new();
let ev = b.fold_double_click(UiEvent::WindowFocused(true));
assert_eq!(ev, UiEvent::WindowFocused(true));
}
#[test]
fn poll_events_drains_queue_fifo() {
let b = MacBackend::new();
b.push_event(UiEvent::MouseDown {
widget: None,
button: crate::MouseButton::Left,
position: Point::new(1.0, 2.0),
modifiers: Modifiers::default(),
});
b.push_event(UiEvent::WindowFocused(true));
let mut b = b;
let evs = b.poll_events();
assert_eq!(evs.len(), 2);
assert!(matches!(evs[0], UiEvent::MouseDown { .. }));
assert!(matches!(evs[1], UiEvent::WindowFocused(true)));
assert!(b.poll_events().is_empty());
}
#[test]
fn waker_payload_reaches_poll_events_as_user_event() {
let backend = MacBackend::new();
let waker = Backend::waker(&backend);
let handle = std::thread::spawn(move || {
waker(crate::UserPayload::new("from background".to_string()));
});
handle.join().expect("background thread must not panic");
let mut backend = backend;
let events = backend.poll_events();
assert_eq!(events.len(), 1);
match &events[0] {
UiEvent::User(payload) => {
assert_eq!(
payload.downcast_ref::<String>().map(String::as_str),
Some("from background")
);
}
other => panic!("expected UiEvent::User, got {other:?}"),
}
}
#[test]
fn waker_does_not_panic_before_wake_callback_is_installed() {
let backend = MacBackend::new();
let waker = Backend::waker(&backend);
waker(crate::UserPayload::new(1_i32));
}
#[test]
fn enter_frame_scope_saves_and_restores_ptr() {
let mut b = MacBackend::new();
assert!(b.current_cg().is_null());
let dummy: CGContextRef = 0x1 as CGContextRef;
b.enter_frame_scope(dummy, |inner| {
assert_eq!(inner.current_cg(), dummy);
});
assert!(b.current_cg().is_null());
}
#[test]
fn line_height_picks_up_set_current_line_height_via_font_install() {
let mut b = MacBackend::new();
let font = super::super::text::make_font("Menlo", 14.0).expect("Menlo installed");
b.set_current_font(font);
assert!(b.line_height() > 0.0);
assert!(b.char_width() > 0.0);
assert!(b.current_font.is_some());
}
#[test]
fn set_current_line_height_and_char_width_override_font_derived_defaults() {
let mut b = MacBackend::new();
let font = super::super::text::make_font("Menlo", 14.0).expect("Menlo installed");
b.set_current_font(font);
let font_derived_line_height = b.line_height();
let font_derived_char_width = b.char_width();
let overridden_line_height = font_derived_line_height + 3.0;
let overridden_char_width = font_derived_char_width + 1.5;
b.set_current_line_height(overridden_line_height as f64);
b.set_current_char_width(overridden_char_width as f64);
assert_eq!(b.line_height(), overridden_line_height);
assert_eq!(b.char_width(), overridden_char_width);
assert!(b.current_font.is_some());
}
#[test]
fn begin_window_drag_false_without_window() {
let mut b = MacBackend::new();
assert!(!Backend::begin_window_drag(&mut b));
}
#[test]
fn begin_window_drag_false_without_pending_press() {
let mut b = MacBackend::new();
assert!(b.pending_window_press.take().is_none());
}
#[test]
fn toggle_window_maximize_false_without_window() {
let mut b = MacBackend::new();
assert!(!Backend::toggle_window_maximize(&mut b));
}
#[test]
fn is_maximized_err_without_window() {
let b = MacBackend::new();
assert!(matches!(
crate::backend::WindowControl::is_maximized(&b),
Err(BackendError::Unsupported)
));
}
#[test]
fn set_decorated_err_without_window() {
let mut b = MacBackend::new();
assert!(matches!(
crate::backend::WindowControl::set_decorated(&mut b, false),
Err(BackendError::Unsupported)
));
}
#[test]
fn set_cursor_false_without_window() {
let mut b = MacBackend::new();
assert!(!Backend::set_cursor(&mut b, PointerShape::Default));
assert!(!Backend::set_cursor(
&mut b,
PointerShape::Resize(ResizeEdge::North)
));
}
#[test]
fn titlebar_control_inset_default_without_window() {
let b = MacBackend::new();
assert_eq!(Backend::titlebar_control_inset(&b), Rect::default());
}
#[test]
fn titlebar_control_inset_is_narrow_on_a_live_window() {
use objc2::msg_send;
use objc2_app_kit::{NSBackingStoreType, NSWindowStyleMask};
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let window_width = 800.0_f64;
let content_rect = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(window_width, 600.0));
let style = NSWindowStyleMask::Titled
| NSWindowStyleMask::Closable
| NSWindowStyleMask::Resizable
| NSWindowStyleMask::Miniaturizable;
let window: Retained<NSWindow> = unsafe {
msg_send![
mtm.alloc::<NSWindow>(),
initWithContentRect: content_rect,
styleMask: style,
backing: NSBackingStoreType::Buffered,
defer: false,
]
};
let mut b = MacBackend::new();
b.set_window(window);
let inset = Backend::titlebar_control_inset(&b);
assert!(
inset.width < 150.0,
"traffic-light cluster width should be well under 150pt (HIG spacing is ~78pt) — \
got {inset:?}; a width anywhere near the window width means this is reading the \
full-width NSTitlebarView again, not the button cluster (#1154)",
);
assert!(
(inset.width as f64) < window_width,
"inset width {} must be strictly less than the window width {window_width} — equal \
(or greater) is the exact #1154 symptom",
inset.width,
);
}
#[test]
fn titlebar_control_inset_collapses_to_default_when_buttons_hidden() {
use objc2::msg_send;
use objc2_app_kit::{NSBackingStoreType, NSWindowButton, NSWindowStyleMask};
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let content_rect = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(800.0, 600.0));
let style = NSWindowStyleMask::Titled
| NSWindowStyleMask::Closable
| NSWindowStyleMask::Resizable
| NSWindowStyleMask::Miniaturizable;
let window: Retained<NSWindow> = unsafe {
msg_send![
mtm.alloc::<NSWindow>(),
initWithContentRect: content_rect,
styleMask: style,
backing: NSBackingStoreType::Buffered,
defer: false,
]
};
let button_kinds = [
NSWindowButton::CloseButton,
NSWindowButton::MiniaturizeButton,
NSWindowButton::ZoomButton,
];
let buttons: Vec<_> = button_kinds
.iter()
.filter_map(|kind| window.standardWindowButton(*kind))
.collect();
assert!(
!buttons.is_empty(),
"a Titled/Closable/Resizable/Miniaturizable window must vend at least one standard \
window button to hide for this test to mean anything",
);
let mut b = MacBackend::new();
b.set_window(window);
let visible_inset = Backend::titlebar_control_inset(&b);
assert!(
visible_inset.width > 0.0,
"expected a non-empty inset before hiding any buttons, got {visible_inset:?}",
);
for button in &buttons {
button.setHidden(true);
}
assert_eq!(
Backend::titlebar_control_inset(&b),
Rect::default(),
"hidden traffic lights must report Rect::default(), not a stale cluster rect — a \
non-default result here is the #1177 'grey rectangle' bug",
);
for button in &buttons {
button.setHidden(false);
}
assert_eq!(
Backend::titlebar_control_inset(&b),
visible_inset,
"the inset must report the same cluster rect again once the buttons un-hide",
);
}
#[test]
fn mac_cursor_kind_maps_every_variant() {
let expected = [
MacCursorKind::Arrow,
MacCursorKind::ResizeUpDown,
MacCursorKind::ResizeUpDown,
MacCursorKind::ResizeLeftRight,
MacCursorKind::ResizeLeftRight,
MacCursorKind::Arrow,
MacCursorKind::Arrow,
MacCursorKind::Arrow,
MacCursorKind::Arrow,
];
let shapes = crate::desktop::all_pointer_shapes();
assert_eq!(
shapes.len(),
expected.len(),
"every PointerShape must have an expected cursor kind — a new variant means a new \
row here, not a silently-truncated zip"
);
for (shape, expected) in shapes.iter().zip(expected.iter()) {
assert_eq!(
mac_cursor_kind(*shape),
*expected,
"PointerShape {shape:?} did not map to the expected cursor kind",
);
}
}
#[test]
fn union_ns_rect_covers_both_inputs() {
let a = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(10.0, 10.0));
let b = NSRect::new(NSPoint::new(20.0, 5.0), NSSize::new(10.0, 20.0));
let u = union_ns_rect(a, b);
assert_eq!(u.origin.x, 0.0);
assert_eq!(u.origin.y, 0.0);
assert_eq!(u.size.width, 30.0); assert_eq!(u.size.height, 25.0);
let u_swapped = union_ns_rect(b, a);
assert_eq!(u.origin.x, u_swapped.origin.x);
assert_eq!(u.origin.y, u_swapped.origin.y);
assert_eq!(u.size.width, u_swapped.size.width);
assert_eq!(u.size.height, u_swapped.size.height);
}
#[test]
fn union_ns_rect_with_nested_rect_keeps_the_larger_one() {
let outer = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(100.0, 50.0));
let inner = NSRect::new(NSPoint::new(10.0, 10.0), NSSize::new(5.0, 5.0));
let u = union_ns_rect(outer, inner);
assert_eq!(u.origin.x, 0.0);
assert_eq!(u.origin.y, 0.0);
assert_eq!(u.size.width, 100.0);
assert_eq!(u.size.height, 50.0);
}
#[test]
#[allow(deprecated)]
fn mac_cursor_for_shape_vends_the_kind_it_maps_to() {
if objc2_foundation::MainThreadMarker::new().is_none() {
return;
}
let arrow = mac_cursor_for_shape(PointerShape::Default);
let up_down = mac_cursor_for_shape(PointerShape::Resize(ResizeEdge::North));
let left_right = mac_cursor_for_shape(PointerShape::Resize(ResizeEdge::East));
assert_eq!(
Retained::as_ptr(&arrow),
Retained::as_ptr(&NSCursor::arrowCursor())
);
assert_eq!(
Retained::as_ptr(&up_down),
Retained::as_ptr(&NSCursor::resizeUpDownCursor())
);
assert_eq!(
Retained::as_ptr(&left_right),
Retained::as_ptr(&NSCursor::resizeLeftRightCursor())
);
assert_ne!(Retained::as_ptr(&arrow), Retained::as_ptr(&up_down));
assert_ne!(Retained::as_ptr(&arrow), Retained::as_ptr(&left_right));
assert_ne!(Retained::as_ptr(&up_down), Retained::as_ptr(&left_right));
}
#[test]
fn backend_caps_declares_window_chrome_and_pointer_cursor() {
let b = MacBackend::new();
let caps = b.backend_caps();
assert!(caps.window_chrome);
assert!(caps.pointer_cursor);
}
#[test]
fn backend_caps_declares_native_dialogs() {
let b = MacBackend::new();
assert!(b.backend_caps().native_dialogs);
}
fn bordered_overflow_list(content_width: usize, h_scroll: usize, bordered: bool) -> ListView {
ListView {
id: crate::types::WidgetId::new("l"),
title: None,
items: vec![crate::ListItem {
text: crate::types::StyledText::plain("x".repeat(content_width)),
detail: None,
icon: None,
decoration: crate::types::Decoration::default(),
}],
selected_idx: 0,
scroll_offset: 0,
has_focus: true,
bordered,
h_scroll,
max_content_width: Some(content_width),
show_v_scrollbar: false,
}
}
#[test]
fn list_hscrollbar_insets_track_for_bordered_list() {
let mut b = MacBackend::new();
b.current_char_width = 8.0;
b.current_line_height = 16.0;
let rect = Rect::new(0.0, 0.0, 200.0, 100.0);
let flat = bordered_overflow_list(40, 0, false);
let flat_sb = b
.list_hscrollbar(rect, &flat)
.expect("overflowing content should yield a scrollbar");
assert_eq!(flat_sb.track.x, 0.0);
assert_eq!(flat_sb.track.width, 200.0);
let bordered = bordered_overflow_list(40, 0, true);
let bordered_sb = b
.list_hscrollbar(rect, &bordered)
.expect("overflowing content should yield a scrollbar");
assert_eq!(bordered_sb.track.x, 8.0);
assert_eq!(bordered_sb.track.width, 184.0);
assert_eq!(bordered_sb.track.y, 100.0 - 2.0 * 16.0);
assert!(bordered_sb.track.x >= rect.x + 8.0);
assert!(bordered_sb.track.x + bordered_sb.track.width <= rect.x + rect.width - 8.0);
}
fn sample_minimap() -> crate::primitives::minimap::Minimap {
crate::primitives::minimap::Minimap {
id: WidgetId::new("minimap"),
lines: (0..40)
.map(|i| crate::primitives::minimap::MinimapLine {
text: format!("line {i}"),
line_idx: i,
})
.collect(),
syntax_spans: Vec::new(),
visible_row_start: 0,
visible_row_count: 10,
total_buffer_lines: 40,
}
}
#[test]
fn draw_minimap_paints_and_reports_painted() {
use super::super::headless::BitmapSurface;
use super::super::text::make_font;
const W: u32 = 100;
const H: u32 = 200;
let surface = BitmapSurface::new(W, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
let mut b = MacBackend::new();
b.set_current_font(make_font("Menlo", 12.0).expect("Menlo installed"));
let minimap = sample_minimap();
let rect = Rect::new(0.0, 0.0, 20.0, H as f32);
let result = std::cell::RefCell::new(None);
b.enter_frame_scope(surface.context_ptr(), |backend| {
*result.borrow_mut() = Some(backend.draw_minimap(rect, &minimap));
});
let result = result
.into_inner()
.expect("draw_minimap ran inside the frame scope");
assert!(
result.painted,
"macOS now has a Core Graphics/Core Text minimap rasteriser (#961) -- \
`painted` must report `true`"
);
assert!(
!result.layout.visible_lines.is_empty(),
"the layout must be real geometry"
);
assert!(
b.zones().iter().any(|z| z.id == minimap.id),
"a click zone must be registered so a host can route clicks"
);
let painted_any = (0..W).any(|x| {
(0..H).any(|y| {
let (r, g, bl, _) = surface.pixel(x, y);
(r, g, bl) != (255, 255, 255)
})
});
assert!(painted_any, "draw_minimap must paint non-background pixels");
}
#[test]
fn minimap_layout_agrees_with_draw_minimap() {
use super::super::headless::BitmapSurface;
use super::super::text::make_font;
let surface = BitmapSurface::new(100, 200);
let mut b = MacBackend::new();
b.set_current_font(make_font("Menlo", 12.0).expect("Menlo installed"));
let minimap = sample_minimap();
let rect = Rect::new(0.0, 0.0, 20.0, 100.0);
let painted = std::cell::RefCell::new(None);
b.enter_frame_scope(surface.context_ptr(), |backend| {
*painted.borrow_mut() = Some(backend.draw_minimap(rect, &minimap));
});
let painted = painted
.into_inner()
.expect("draw_minimap ran inside the frame scope");
let layout_only = b.minimap_layout(rect, &minimap);
assert_eq!(painted.layout, layout_only);
}
#[test]
fn draw_image_from_empty_bytes_reports_unsupported() {
let mut b = MacBackend::new();
let image = crate::primitives::image::Image {
id: WidgetId::new("logo"),
source: crate::primitives::image::ImageSource::Bytes(Vec::new()),
intrinsic_size: Some((24, 24)),
fit: crate::primitives::image::ImageFit::Contain,
fallback_text: "[Q]".into(),
};
let rect = Rect::new(0.0, 0.0, 24.0, 24.0);
let result = b.draw_image(rect, &image);
assert_eq!(
result,
crate::backend::ImagePaintResult::Unsupported,
"an empty byte source has nothing to decode -- this must be a clean \
Unsupported result, not a panic"
);
assert!(
b.zones().iter().any(|z| z.id == image.id),
"a click/hover zone must still be registered even though nothing painted"
);
}
fn tiny_png_bytes() -> Vec<u8> {
let mut png = Vec::new();
png.extend_from_slice(b"\x89PNG\r\n\x1a\n");
fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
let mut body = Vec::with_capacity(4 + data.len());
body.extend_from_slice(kind);
body.extend_from_slice(data);
out.extend_from_slice(&body);
out.extend_from_slice(&crc32(&body).to_be_bytes());
}
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&4u32.to_be_bytes());
ihdr.extend_from_slice(&4u32.to_be_bytes());
ihdr.extend_from_slice(&[8, 2, 0, 0, 0]);
chunk(&mut png, b"IHDR", &ihdr);
#[allow(clippy::same_item_push)]
let raw = {
let mut raw = Vec::new();
for _ in 0..4 {
raw.push(0u8);
for _ in 0..4 {
raw.extend_from_slice(&[0x00, 0xff, 0x00]);
}
}
raw
};
let idat = zlib_store(&raw);
chunk(&mut png, b"IDAT", &idat);
chunk(&mut png, b"IEND", &[]);
png
}
fn zlib_store(data: &[u8]) -> Vec<u8> {
let mut out = vec![0x78, 0x01];
let len = data.len() as u16;
out.push(1);
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&(!len).to_le_bytes());
out.extend_from_slice(data);
out.extend_from_slice(&adler32(data).to_be_bytes());
out
}
fn adler32(data: &[u8]) -> u32 {
let mut a: u32 = 1;
let mut b: u32 = 0;
for &byte in data {
a = (a + byte as u32) % 65521;
b = (b + a) % 65521;
}
(b << 16) | a
}
fn crc32(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
}
}
!crc
}
#[test]
fn draw_image_paints_and_reports_painted() {
use super::super::headless::BitmapSurface;
const W: u32 = 64;
const H: u32 = 64;
let surface = BitmapSurface::new(W, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
let mut b = MacBackend::new();
let image = crate::primitives::image::Image {
id: WidgetId::new("logo"),
source: crate::primitives::image::ImageSource::Bytes(tiny_png_bytes()),
intrinsic_size: Some((4, 4)),
fit: crate::primitives::image::ImageFit::Fill,
fallback_text: "[Q]".into(),
};
let rect = Rect::new(10.0, 10.0, 20.0, 20.0);
let result = std::cell::RefCell::new(None);
b.enter_frame_scope(surface.context_ptr(), |backend| {
*result.borrow_mut() = Some(backend.draw_image(rect, &image));
});
let result = result
.into_inner()
.expect("draw_image ran inside the frame scope");
assert_eq!(
result,
crate::backend::ImagePaintResult::Painted,
"macOS now has a Core Graphics/ImageIO image rasteriser (#962) -- a \
decodable source must report Painted"
);
assert!(
b.zones().iter().any(|z| z.id == image.id),
"a click/hover zone must be registered"
);
let painted_any = (0..W).any(|x| {
(0..H).any(|y| {
let (r, g, bl, _) = surface.pixel(x, y);
(r, g, bl) != (255, 255, 255)
})
});
assert!(painted_any, "draw_image must paint non-background pixels");
}
fn text_region(id: &str, x: f32, y: f32, w: f32, h: f32, lines: Vec<&str>) -> TextRegion {
TextRegion {
id: WidgetId::new(id),
bounds: crate::event::Rect::new(x, y, w, h),
lines: lines.into_iter().map(String::from).collect(),
}
}
fn font() -> CTFont {
super::super::text::make_font("Menlo", 14.0).expect("Menlo installed")
}
#[test]
fn mac_backend_declares_text_selection_capability() {
let b = MacBackend::new();
assert!(
b.backend_caps().text_selection,
"#803: MacBackend must declare text_selection now that register_text_region/\
cancel_text_selection_drag are both overridden"
);
}
#[test]
fn mac_backend_declares_app_font_registration_capability() {
let b = MacBackend::new();
assert!(
b.backend_caps().app_font_registration,
"#929: MacBackend must declare app_font_registration now that \
register_font_from_memory/set_nerd_font_fallback are both overridden"
);
}
#[test]
fn set_nerd_font_fallback_then_set_current_font_carries_the_fallback() {
use core_foundation::base::TCFType;
use core_foundation::string::CFString;
use core_text::font_descriptor::kCTFontCascadeListAttribute;
let mut b = MacBackend::new();
b.set_nerd_font_fallback("Helvetica");
b.set_current_font(font());
let attrs = b
.current_font
.as_ref()
.expect("set_current_font must install a font")
.copy_descriptor()
.attributes()
.to_untyped();
let cascade_key = unsafe { CFString::wrap_under_get_rule(kCTFontCascadeListAttribute) };
assert!(
attrs.contains_key(&cascade_key.as_CFTypeRef()),
"a fallback set before set_current_font must still be applied to the installed font"
);
}
#[test]
fn set_current_font_then_set_nerd_font_fallback_retroactively_applies() {
use core_foundation::base::TCFType;
use core_foundation::string::CFString;
use core_text::font_descriptor::kCTFontCascadeListAttribute;
let mut b = MacBackend::new();
b.set_current_font(font());
b.set_nerd_font_fallback("Helvetica");
let attrs = b
.current_font
.as_ref()
.expect("current_font must still be installed")
.copy_descriptor()
.attributes()
.to_untyped();
let cascade_key = unsafe { CFString::wrap_under_get_rule(kCTFontCascadeListAttribute) };
assert!(
attrs.contains_key(&cascade_key.as_CFTypeRef()),
"set_nerd_font_fallback must retroactively apply to an already-installed \
current_font"
);
}
#[test]
fn mac_backend_register_font_from_memory_rejects_bytes_that_are_not_a_font() {
let mut b = MacBackend::new();
let garbage = [0u8; 64];
assert!(
b.register_font_from_memory(&garbage).is_none(),
"64 zero bytes are not a parseable font — must report None, not a fabricated family"
);
}
#[test]
fn mac_register_text_region_and_begin_frame_clears_it() {
let mut b = MacBackend::new();
b.register_text_region(text_region("r", 0.0, 0.0, 100.0, 50.0, vec![]));
assert_eq!(b.text_regions().len(), 1);
b.begin_frame(Viewport::new(200.0, 100.0, 1.0));
assert!(
b.text_regions().is_empty(),
"begin_frame must clear the previous frame's registered regions"
);
}
#[test]
fn mac_clear_selection_display_clears_active_selection_only() {
let mut b = MacBackend::new();
assert!(b.active_text_selection().is_none());
b.set_active_text_selection(
WidgetId::new("r"),
Point::new(0.0, 0.0),
Point::new(50.0, 20.0),
);
assert!(b.active_text_selection().is_some());
b.clear_selection_display();
assert!(b.active_text_selection().is_none());
}
#[test]
fn mac_clear_text_selection_also_ends_a_text_selection_drag() {
let mut b = MacBackend::new();
b.drag_state
.borrow_mut()
.begin(crate::dispatch::DragTarget::TextSelection {
region: WidgetId::new("r"),
anchor: Point::new(0.0, 0.0),
});
b.set_active_text_selection(
WidgetId::new("r"),
Point::new(0.0, 0.0),
Point::new(10.0, 10.0),
);
b.clear_text_selection();
assert!(b.active_text_selection().is_none());
assert!(
!b.drag_state.borrow().is_active(),
"clear_text_selection must also end an in-progress TextSelection drag"
);
}
#[test]
fn mac_cancel_text_selection_drag_preserves_the_displayed_selection() {
let mut b = MacBackend::new();
b.drag_state
.borrow_mut()
.begin(crate::dispatch::DragTarget::TextSelection {
region: WidgetId::new("r"),
anchor: Point::new(0.0, 0.0),
});
b.set_active_text_selection(
WidgetId::new("r"),
Point::new(0.0, 0.0),
Point::new(10.0, 10.0),
);
b.cancel_text_selection_drag_impl();
assert!(!b.drag_state.borrow().is_active());
assert!(
b.active_text_selection().is_some(),
"cancel_text_selection_drag must not clear the displayed selection"
);
}
#[test]
fn mac_select_all_text_region_targets_the_sole_region() {
let mut b = MacBackend::new();
b.register_text_region(text_region("body", 0.0, 0.0, 10.0, 5.0, vec![]));
assert!(b.select_all_text_region());
let sel = b
.active_text_selection()
.expect("selection should be active after select_all_text_region");
assert_eq!(sel.anchor, Point::new(0.0, 0.0));
assert_eq!(sel.focus, Point::new(10.0, 5.0));
}
#[test]
fn mac_select_all_text_region_returns_false_with_no_regions() {
let mut b = MacBackend::new();
assert!(!b.select_all_text_region());
assert!(b.active_text_selection().is_none());
}
#[test]
fn mac_extract_selection_text_single_row() {
let mut b = MacBackend::new();
b.set_current_font(font());
let line = "The quick brown fox jumps over the lazy dog.";
let width = (line.chars().count() as f32 + 2.0) * b.char_width();
b.register_text_region(text_region("body", 0.0, 0.0, width, 100.0, vec![line]));
b.set_active_text_selection(
WidgetId::new("body"),
Point::new(0.0, 0.0),
Point::new(width, 1.0),
);
let text = b.extract_selection_text();
assert_eq!(text, line);
}
#[test]
fn mac_extract_selection_text_empty_when_no_selection() {
let b = MacBackend::new();
assert_eq!(b.extract_selection_text(), "");
}
#[test]
fn mac_extract_selection_text_empty_when_no_lines() {
let mut b = MacBackend::new();
b.set_current_font(font());
b.register_text_region(text_region("body", 0.0, 0.0, 200.0, 32.0, vec![]));
b.set_active_text_selection(
WidgetId::new("body"),
Point::new(0.0, 0.0),
Point::new(200.0, 32.0),
);
assert_eq!(
b.extract_selection_text(),
"",
"a region with no `lines` content has nothing to extract"
);
}
#[test]
fn mac_apply_selection_highlight_paints_over_the_background() {
use super::super::headless::BitmapSurface;
use crate::primitives::status_bar::{StatusBar, StatusBarSegment};
use crate::types::Color;
const W: u32 = 200;
const H: u32 = 32;
const BG: Color = Color::rgb(10, 10, 10);
const TEXT: &str = "hello world";
let surface = BitmapSurface::new(W, H);
let mut b = MacBackend::new();
b.set_current_font(font());
b.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let layout = std::cell::RefCell::new(None);
b.enter_frame_scope(surface.context_ptr(), |bk| {
let l = bk.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&StatusBar {
id: WidgetId::new("bg"),
left_segments: vec![StatusBarSegment {
text: TEXT.into(),
fg: Color::rgb(255, 255, 255),
bg: BG,
bold: false,
action_id: None,
}],
right_segments: vec![],
},
&crate::InteractionState::new(),
);
*layout.borrow_mut() = Some(l);
});
let layout = layout.into_inner().expect("draw_status_bar ran");
let seg = layout
.visible_segments
.first()
.expect("the single left segment is visible in a 200pt-wide bar");
let probe_x = (seg.bounds.x + seg.bounds.width) as u32 + 2;
assert!(
probe_x < W,
"probe column {probe_x} must stay inside the {W}pt surface — the \
'{TEXT}' segment measured wider than expected ({seg:?})"
);
let probe_y = 2;
let line_h = b.line_height();
let unselected_y = line_h as u32 + 2;
assert!(
unselected_y < H,
"the unselected probe row {unselected_y} must stay inside the \
{H}pt surface (line_height = {line_h})"
);
b.register_text_region(text_region("bg", 0.0, 0.0, W as f32, H as f32, vec![TEXT]));
b.set_active_text_selection(
WidgetId::new("bg"),
Point::new(0.0, 0.0),
Point::new(W as f32, 1.0),
);
let before = surface.pixel(probe_x, probe_y);
let before_unselected = surface.pixel(probe_x, unselected_y);
assert_eq!(
before,
(BG.r, BG.g, BG.b, 255),
"the probe pixel must sit on the bar's flat background fill, not on \
a glyph — otherwise the blend below is measured against the wrong base"
);
b.enter_frame_scope(surface.context_ptr(), |bk| {
bk.apply_selection_highlight();
});
let after = surface.pixel(probe_x, probe_y);
let after_unselected = surface.pixel(probe_x, unselected_y);
assert_ne!(
before, after,
"apply_selection_highlight must paint over the background at a selected pixel"
);
let (ar, ag, ab) = (after.0 as i32, after.1 as i32, after.2 as i32);
assert!(
ab > before.2 as i32 + 20,
"the highlight is translucent blue, so the blue channel must rise \
noticeably at a selected pixel: before={before:?} after={after:?}"
);
assert!(
ab > ar + 20 && ab > ag + 20,
"blue must end up the dominant channel after a translucent-blue \
highlight: after={after:?}"
);
assert_eq!(
before_unselected, after_unselected,
"the highlight must be clipped to the selected row — row at y={unselected_y} \
is outside the one-row selection and must be untouched"
);
}
#[test]
fn mac_backend_paint_surface_fill_rect_paints_solid_color() {
use super::super::headless::BitmapSurface;
use crate::types::Color;
const W: u32 = 32;
const H: u32 = 32;
let surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_current_font(font());
let red = Color::rgb(200, 20, 20);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.surface_fill_rect(Rect::new(0.0, 0.0, W as f32, H as f32), red);
});
backend.end_frame();
let (r, g, b, _a) = surface.pixel(5, 5);
assert_eq!(
(r, g, b),
(red.r, red.g, red.b),
"surface_fill_rect must paint the solid color it was given"
);
}
#[test]
fn mac_backend_paint_surface_verbs_do_not_panic() {
use super::super::headless::BitmapSurface;
use crate::types::Color;
const W: u32 = 64;
const H: u32 = 64;
let surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_current_font(font());
let viewport = Viewport::new(W as f32, H as f32, 1.0);
backend.surface_begin_frame(viewport);
assert_eq!(
backend.surface_viewport(),
viewport,
"surface_viewport must forward to Backend::begin_frame's stored value"
);
assert_eq!(
backend.surface_line_height(),
backend.chrome_line_height as f32
);
assert_eq!(
backend.surface_char_width(),
backend.chrome_char_width as f32
);
backend.enter_frame_scope(surface.context_ptr(), |b| {
let blue = Color::rgb(20, 20, 200);
let white = Color::rgb(255, 255, 255);
b.surface_stroke_rect(Rect::new(0.0, 0.0, 40.0, 40.0), blue, 2.0);
b.surface_draw_line(Point::new(0.0, 0.0), Point::new(40.0, 40.0), blue, 1.0);
b.surface_push_clip(Rect::new(0.0, 0.0, 40.0, 40.0));
b.surface_draw_text_run(Rect::new(2.0, 2.0, 30.0, 10.0), "hi", white);
b.surface_pop_clip();
let (w, h) = b.surface_measure_text("hi");
assert!(
w > 0.0 && h > 0.0,
"surface_measure_text must report a nonzero footprint for \
non-empty text: got ({w}, {h})"
);
let image = crate::primitives::image::Image {
id: WidgetId::new("test:native-surface-image"),
source: crate::primitives::image::ImageSource::Bytes(Vec::new()),
intrinsic_size: Some((8, 8)),
fit: crate::primitives::image::ImageFit::Contain,
fallback_text: "[i]".to_string(),
};
let _ = b.surface_draw_image(Rect::new(0.0, 40.0, 8.0, 8.0), &image);
});
backend.surface_end_frame();
}
#[test]
fn mac_backend_paint_surface_fill_rounded_rect_clips_the_corners() {
use super::super::headless::BitmapSurface;
use crate::types::Color;
const W: u32 = 40;
const H: u32 = 40;
let surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_current_font(font());
let red = Color::rgb(200, 20, 20);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.surface_fill_rounded_rect(Rect::new(0.0, 0.0, W as f32, H as f32), 15.0, red);
});
backend.end_frame();
let (r, g, b, _a) = surface.pixel(20, 20);
assert_eq!(
(r, g, b),
(red.r, red.g, red.b),
"surface_fill_rounded_rect must paint the solid color at the box's centre"
);
let (r, g, b, _a) = surface.pixel(1, 1);
assert_eq!(
(r, g, b),
(0, 0, 0),
"a corner pixel well inside a radius-15 fillet on a 40x40 box must stay \
untouched — otherwise this is just `surface_fill_rect` under a new name"
);
}
#[test]
fn mac_backend_paint_surface_fill_rect_alpha_blends_with_the_background() {
use super::super::headless::BitmapSurface;
use crate::types::Color;
const W: u32 = 40;
const H: u32 = 40;
let surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_current_font(font());
let white = Color::rgb(255, 255, 255);
let red = Color::rgb(200, 20, 20);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.surface_fill_rect(Rect::new(0.0, 0.0, W as f32, H as f32), white);
b.surface_fill_rect_alpha(Rect::new(0.0, 0.0, W as f32, H as f32), red, 0.5);
});
backend.end_frame();
let (r, g, b, _a) = surface.pixel(20, 20);
assert!(
(r, g, b) != (white.r, white.g, white.b) && (r, g, b) != (red.r, red.g, red.b),
"a 50%-alpha fill over an opaque background must land a real blend, \
not the background or the fill colour verbatim: got ({r}, {g}, {b})"
);
assert!(
(200..245).contains(&r),
"red channel should sit between the fill's 200 and white's 255: got {r}"
);
assert!(
(100..160).contains(&g) && (100..160).contains(&b),
"green/blue channels should sit roughly halfway between the fill's 20 \
and white's 255: got ({g}, {b})"
);
}
#[test]
fn mac_backend_paint_surface_draw_text_run_with_role_uses_the_requested_fonts_size() {
use super::super::headless::BitmapSurface;
use crate::types::Color;
fn ink_pixel_count(surface: &BitmapSurface) -> u32 {
let mut count = 0;
for y in 0..80 {
for x in 0..80 {
let (r, g, b, _a) = surface.pixel(x, y);
if r as u32 + g as u32 + b as u32 > 0 {
count += 1;
}
}
}
count
}
const W: u32 = 80;
const H: u32 = 80;
let white = Color::rgb(255, 255, 255);
let chrome_surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_current_font(
super::super::text::make_font("Menlo", 6.0).expect("Menlo installed"),
);
backend.set_chrome_font(
super::super::text::make_font("Menlo", 48.0).expect("Menlo installed"),
);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.enter_frame_scope(chrome_surface.context_ptr(), |b| {
b.surface_draw_text_run_with_role(
Rect::new(2.0, 2.0, 76.0, 76.0),
"A",
white,
crate::FontRole::Chrome,
false,
);
});
backend.end_frame();
let chrome_ink = ink_pixel_count(&chrome_surface);
let editor_surface = BitmapSurface::new(W, H);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.enter_frame_scope(editor_surface.context_ptr(), |b| {
b.surface_draw_text_run_with_role(
Rect::new(2.0, 2.0, 76.0, 76.0),
"A",
white,
crate::FontRole::Editor,
false,
);
});
backend.end_frame();
let editor_ink = ink_pixel_count(&editor_surface);
assert!(
chrome_ink > 0 && editor_ink > 0,
"both roles must paint real ink: chrome={chrome_ink}, editor={editor_ink}"
);
assert!(
chrome_ink > editor_ink * 3,
"a 48pt chrome font must paint far more ink than a 6pt editor font for the \
same glyph if `role` really selects a different live font: \
chrome={chrome_ink}, editor={editor_ink}"
);
let italic_surface = BitmapSurface::new(W, H);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.enter_frame_scope(italic_surface.context_ptr(), |b| {
b.surface_draw_text_run_with_role(
Rect::new(2.0, 2.0, 76.0, 76.0),
"A",
white,
crate::FontRole::Chrome,
true,
);
});
backend.end_frame();
}
#[test]
fn mac_backend_paint_surface_draw_icon_glyph_paints_real_ink() {
use super::super::headless::BitmapSurface;
use crate::types::Color;
const W: u32 = 40;
const H: u32 = 40;
let surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_current_font(font());
let white = Color::rgb(255, 255, 255);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.surface_draw_icon_glyph(Rect::new(2.0, 2.0, 30.0, 30.0), "i", white);
});
backend.end_frame();
let mut ink = 0u32;
for y in 0..H {
for x in 0..W {
let (r, g, b, _a) = surface.pixel(x, y);
ink += r as u32 + g as u32 + b as u32;
}
}
assert!(
ink > 0,
"surface_draw_icon_glyph must paint real ink, not silently no-op"
);
}
#[test]
fn mac_backend_draw_form_paints_the_four_field_kinds_macos_used_to_drop() {
use super::super::headless::BitmapSurface;
use crate::primitives::form::{FieldKind, Form, FormField};
use crate::types::StyledText;
const W: u32 = 320;
const H: u32 = 160;
fn field(id: &str, label: &str, kind: FieldKind) -> FormField {
FormField {
id: WidgetId::new(id),
label: StyledText::plain(label),
kind,
hint: StyledText::default(),
disabled: false,
validation: None,
}
}
let form = Form {
id: WidgetId::new("regression-808"),
fields: vec![
field(
"font-size",
"Font size",
FieldKind::Slider {
value: 14.0,
min: 8.0,
max: 32.0,
step: 1.0,
},
),
field(
"accent",
"Accent",
FieldKind::ColorPicker {
value: Color::rgb(0x7a, 0xb4, 0xff),
},
),
field(
"theme",
"Theme",
FieldKind::Dropdown {
options: vec![
StyledText::plain("One Dark"),
StyledText::plain("Solarized"),
],
selected_idx: 1,
},
),
field(
"notes",
"Notes",
FieldKind::TextArea {
value: "release notes".into(),
placeholder: String::new(),
cursor: None,
visible_rows: 3,
},
),
],
focused_field: None,
scroll_offset: 0,
has_focus: false,
};
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
let surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let flayout = backend.form_layout(rect, &form);
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.draw_form(rect, &form);
});
backend.end_frame();
let theme = Theme::default();
let bg = (theme.tab_bar_bg.r, theme.tab_bar_bg.g, theme.tab_bar_bg.b);
for vf in &flayout.visible_fields {
let field = &form.fields[vf.field_idx];
let x0 = (vf.bounds.x + vf.bounds.width * 0.6) as u32;
let x1 = ((vf.bounds.x + vf.bounds.width) as u32).min(W);
let y0 = vf.bounds.y as u32;
let y1 = ((vf.bounds.y + vf.bounds.height) as u32).min(H);
let mut painted_beyond_label = false;
'scan: for y in y0..y1 {
for x in x0..x1 {
let (r, g, b, _a) = surface.pixel(x, y);
if (r, g, b) != bg {
painted_beyond_label = true;
break 'scan;
}
}
}
assert!(
painted_beyond_label,
"field {:?} ({:?}) must paint something in the right ~40% of its \
row (past the label) — this is exactly the #808 silent-drop bug: \
pre-fix, macos::form::draw_form's `_ => {{}}` matched Slider/\
ColorPicker/Dropdown/TextArea and painted only the row background \
there",
field.id, field.kind,
);
}
}
fn find_replace_sample_panel(w: f32, h: f32) -> FindReplacePanel {
let (hit_regions, _input_width) =
crate::primitives::find_replace::compute_hit_regions(50, false, "1 of 3", 2, 2);
FindReplacePanel {
query: "needle".into(),
replacement: String::new(),
show_replace: false,
focus: 0,
cursor: 6,
sel_anchor: None,
match_info: "1 of 3".into(),
case_sensitive: false,
whole_word: false,
use_regex: false,
preserve_case: false,
in_selection: false,
group_bounds: crate::event::Rect::new(0.0, 0.0, w, h),
panel_width: 50,
replace_one_glyph: "R1".into(),
replace_all_glyph: "R*".into(),
hit_regions,
}
}
#[test]
fn mac_backend_draw_find_replace_paints_popup_background() {
use super::super::headless::BitmapSurface;
const W: u32 = 600;
const H: u32 = 200;
let surface = BitmapSurface::new(W, H);
surface.fill(0.0, 0.0, 0.0, 0.0);
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.set_chrome_font(font());
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let panel = find_replace_sample_panel(W as f32, H as f32);
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.draw_find_replace(
crate::event::Rect::new(0.0, 0.0, W as f32, H as f32),
&panel,
);
});
backend.end_frame();
let theme = crate::theme::Theme::default();
let cw = PaintSurface::surface_char_width(&backend).max(1.0);
let lh = PaintSurface::surface_line_height(&backend).max(1.0);
let popup_w = panel.panel_width as f32 * cw;
let popup_h = 3.0 * lh; let popup_x = (panel.group_bounds.x + panel.group_bounds.width - popup_w - 10.0)
.max(panel.group_bounds.x);
let popup_y = panel.group_bounds.y + 2.0;
let probe_x = (popup_x + popup_w - 4.0) as u32;
let probe_y = (popup_y + popup_h - 4.0) as u32;
assert!(
probe_x < W
&& probe_y < H
&& (probe_x as f32) >= popup_x
&& (probe_y as f32) >= popup_y,
"test setup: the probe ({probe_x}, {probe_y}) must sit inside both the \
{W}×{H} surface and the popup ({popup_w}×{popup_h} at ({popup_x}, \
{popup_y}), chrome advance {cw}, line height {lh})",
);
let (r, g, b, _a) = surface.pixel(probe_x, probe_y);
assert_eq!(
(r, g, b),
(theme.surface_bg.r, theme.surface_bg.g, theme.surface_bg.b),
"find/replace popup background must be painted through MacBackend's \
real PaintSurface impl — probed ({probe_x}, {probe_y}) inside the \
popup ({popup_w}×{popup_h} at ({popup_x}, {popup_y}), chrome advance \
{cw}, line height {lh})",
);
}
#[test]
fn mac_backend_draw_find_replace_with_multibyte_query_does_not_panic() {
use super::super::headless::BitmapSurface;
const W: u32 = 600;
const H: u32 = 200;
let surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let mut panel = find_replace_sample_panel(W as f32, H as f32);
panel.query = "café🎉中文".into();
panel.cursor = 3;
panel.sel_anchor = Some(6);
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.draw_find_replace(
crate::event::Rect::new(0.0, 0.0, W as f32, H as f32),
&panel,
);
});
backend.end_frame();
}
#[test]
fn mac_backend_draw_text_input_paints_value_and_cursor() {
use super::super::headless::BitmapSurface;
use crate::primitives::text_input::TextInput;
const W: u32 = 200;
const H: u32 = 40;
let surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let mut ti = TextInput::new(WidgetId::new("regression-1093"));
ti.lines = vec!["hello".to_string()];
ti.cursor_line = 0;
ti.cursor_col = 3;
ti.has_focus = true;
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
let layout_cell = std::cell::RefCell::new(None);
backend.enter_frame_scope(surface.context_ptr(), |b| {
let l = b.draw_text_input(rect, &ti);
*layout_cell.borrow_mut() = Some(l);
});
backend.end_frame();
let layout = layout_cell.into_inner().expect("draw_text_input ran");
let theme = Theme::default();
let bg = (theme.background.r, theme.background.g, theme.background.b);
let cb = layout.content_bounds;
let mut painted_any = false;
'scan: for y in (cb.y as u32)..(((cb.y + cb.height) as u32).min(H)) {
for x in (cb.x as u32)..(((cb.x + 30.0) as u32).min(W)) {
let (r, g, b, _a) = surface.pixel(x, y);
if (r, g, b) != bg {
painted_any = true;
break 'scan;
}
}
}
assert!(
painted_any,
"expected draw_text_input to paint the value's glyphs, not just \
background/border — this is exactly #1093's silent-stub bug",
);
let cb2 = layout.cursor_bounds.expect("cursor visible when has_focus");
let px = (cb2.x + 1.0) as u32;
let py = (cb2.y + cb2.height / 2.0) as u32;
let (r, g, b, _a) = surface.pixel(px, py);
assert_eq!(
(r, g, b),
(theme.cursor.r, theme.cursor.g, theme.cursor.b),
"expected the insert cursor to paint at cursor_bounds in theme.cursor",
);
}
#[test]
fn text_input_layout_cursor_grid_uses_chrome_char_width_not_editor() {
let mut backend = MacBackend::new();
backend.set_current_font(font()); backend.set_ui_font("Menlo 6"); assert!(
(backend.chrome_char_width - backend.current_char_width).abs() > 1.0,
"chrome and editor char widths must differ enough that a grid built from \
the wrong one can't pass by coincidence: chrome={}, editor={}",
backend.chrome_char_width,
backend.current_char_width,
);
let mut ti =
crate::primitives::text_input::TextInput::new(WidgetId::new("chrome-grid-regression"));
ti.lines = vec!["hello".to_string()];
ti.cursor_line = 0;
ti.cursor_col = 3;
ti.has_focus = true;
let rect = Rect::new(0.0, 0.0, 200.0, 40.0);
let layout = backend.text_input_layout(rect, &ti);
let cursor = layout.cursor_bounds.expect("cursor visible when has_focus");
let expected_x = layout.content_bounds.x + 3.0 * backend.chrome_char_width as f32;
let wrong_x = layout.content_bounds.x + 3.0 * backend.current_char_width as f32;
assert!(
(cursor.x - expected_x).abs() < 0.01,
"cursor_bounds.x must sit on the chrome_char_width grid \
`draw_text_input` actually paints glyphs on: got {}, expected {expected_x} \
(the current_char_width grid would have put it at {wrong_x})",
cursor.x,
);
}
use super::super::headless::BitmapSurface;
use crate::primitives::chart::{
Chart as ChartModel, ChartHit, ChartKind, Series, SERIES_COLORS,
};
const CHART_W: u32 = 200;
const CHART_H: u32 = 120;
fn chart_sample_line() -> ChartModel {
ChartModel {
id: WidgetId::new("ch"),
kind: ChartKind::Line,
series: vec![Series {
label: "x".into(),
data: vec![0.0, 1.0, 0.5, 2.0, 1.0],
color: Some(Color::rgb(80, 160, 255)),
fill: false,
}],
x_label: None,
y_label: None,
y_range: Some((0.0, 2.0)),
x_range: None,
show_legend: false,
y_ticks: Some(0),
x_ticks: Some(0),
show_grid: false,
}
}
fn paint_chart_via_backend(
chart: &ChartModel,
hovered: Option<(usize, usize)>,
) -> (BitmapSurface, ChartLayout) {
let surface = BitmapSurface::new(CHART_W, CHART_H);
surface.fill(0.0, 0.0, 0.0, 0.0);
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.begin_frame(Viewport::new(CHART_W as f32, CHART_H as f32, 1.0));
let layout = std::cell::RefCell::new(None);
backend.enter_frame_scope(surface.context_ptr(), |b| {
let l = b.draw_chart(
Rect::new(0.0, 0.0, CHART_W as f32, CHART_H as f32),
chart,
hovered,
None,
);
*layout.borrow_mut() = Some(l);
});
backend.end_frame();
(surface, layout.into_inner().unwrap())
}
#[test]
fn mac_backend_draw_chart_plot_area_paints_background() {
let chart = chart_sample_line();
let (surface, layout) = paint_chart_via_backend(&chart, None);
let theme = Theme::default();
let px = (layout.plot_area.x + layout.plot_area.width - 2.0) as u32;
let py = (layout.plot_area.y + 2.0) as u32;
let (r, g, b, _) = surface.pixel(px, py);
assert_eq!(
(r, g, b),
(theme.background.r, theme.background.g, theme.background.b),
);
}
#[test]
fn mac_backend_draw_chart_line_stroke_paints_series_color() {
let chart = chart_sample_line();
let (surface, layout) = paint_chart_via_backend(&chart, None);
let (_, _, sx, sy) = layout.data_point_positions[1];
let (r, g, b, _) = surface.pixel(sx as u32, sy as u32);
assert!(
b > r,
"expected blue dominant at data point 1 ({}, {}), got ({}, {}, {})",
sx as u32,
sy as u32,
r,
g,
b,
);
}
#[test]
fn mac_backend_draw_chart_hit_test_inside_plot_area_returns_body() {
let chart = chart_sample_line();
let (_surface, layout) = paint_chart_via_backend(&chart, None);
let cx = layout.plot_area.x + layout.plot_area.width * 0.5;
let cy = layout.plot_area.y + layout.plot_area.height * 0.5;
assert_eq!(layout.hit_test(cx, cy), ChartHit::Body(WidgetId::new("ch")),);
}
#[test]
fn mac_backend_draw_chart_hover_marker_painted_when_set() {
let chart = chart_sample_line();
let (surface, layout) = paint_chart_via_backend(&chart, Some((0, 2)));
let (_, _, sx, sy) = layout.data_point_positions[2];
let (r, _, b, _) = surface.pixel(sx as u32, sy as u32);
assert!(b > r);
}
fn chart_bar_series(label: &str, data: Vec<f64>) -> Series {
Series {
label: label.into(),
data,
color: None,
fill: false,
}
}
fn chart_sample_bar(kind: ChartKind, series: Vec<Series>, y_range: (f64, f64)) -> ChartModel {
ChartModel {
id: WidgetId::new("ch"),
kind,
series,
x_label: None,
y_label: None,
y_range: Some(y_range),
x_range: None,
show_legend: false,
y_ticks: Some(0),
x_ticks: Some(0),
show_grid: false,
}
}
#[test]
fn mac_backend_draw_chart_stacked_bar_paints_each_series_in_its_own_color() {
let chart = chart_sample_bar(
ChartKind::Bar,
vec![
chart_bar_series("a", vec![1.0]),
chart_bar_series("b", vec![1.0]),
chart_bar_series("c", vec![1.0]),
],
(0.0, 3.0),
);
let (surface, _layout) = paint_chart_via_backend(&chart, None);
let mid_x = 100;
let (r0, g0, b0, _) = surface.pixel(mid_x, 100);
assert_eq!(
(r0, g0, b0),
(SERIES_COLORS[0].r, SERIES_COLORS[0].g, SERIES_COLORS[0].b)
);
let (r1, g1, b1, _) = surface.pixel(mid_x, 60);
assert_eq!(
(r1, g1, b1),
(SERIES_COLORS[1].r, SERIES_COLORS[1].g, SERIES_COLORS[1].b)
);
let (r2, g2, b2, _) = surface.pixel(mid_x, 20);
assert_eq!(
(r2, g2, b2),
(SERIES_COLORS[2].r, SERIES_COLORS[2].g, SERIES_COLORS[2].b)
);
}
#[test]
fn mac_backend_draw_chart_grouped_bar_paints_series_side_by_side() {
let chart = chart_sample_bar(
ChartKind::BarGrouped,
vec![
chart_bar_series("a", vec![1.0]),
chart_bar_series("b", vec![2.0]),
chart_bar_series("c", vec![3.0]),
],
(0.0, 3.0),
);
let (surface, _layout) = paint_chart_via_backend(&chart, None);
let probe_y = 100;
let (r0, g0, b0, _) = surface.pixel(43, probe_y);
assert_eq!(
(r0, g0, b0),
(SERIES_COLORS[0].r, SERIES_COLORS[0].g, SERIES_COLORS[0].b),
"leftmost sub-bar should be series 0"
);
let (r1, g1, b1, _) = surface.pixel(100, probe_y);
assert_eq!(
(r1, g1, b1),
(SERIES_COLORS[1].r, SERIES_COLORS[1].g, SERIES_COLORS[1].b),
"middle sub-bar should be series 1"
);
let (r2, g2, b2, _) = surface.pixel(157, probe_y);
assert_eq!(
(r2, g2, b2),
(SERIES_COLORS[2].r, SERIES_COLORS[2].g, SERIES_COLORS[2].b),
"rightmost sub-bar should be series 2"
);
}
#[test]
fn mac_backend_draw_chart_hover_marker_does_not_escape_chart_rect() {
let chart = chart_sample_bar(
ChartKind::Sparkline,
vec![chart_bar_series("a", vec![10.0, 0.0])],
(0.0, 10.0),
);
let canvas_w = 40;
let canvas_h = 40;
let sentinel = Color::rgb(0, 0, 0);
let (chart_x, chart_y, chart_w, chart_h) = (10.0_f32, 10.0_f32, 20.0_f32, 20.0_f32);
let surface = BitmapSurface::new(canvas_w, canvas_h);
surface.fill(0.0, 0.0, 0.0, 1.0);
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.begin_frame(Viewport::new(canvas_w as f32, canvas_h as f32, 1.0));
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.surface_fill_rect(
Rect::new(0.0, 0.0, canvas_w as f32, canvas_h as f32),
sentinel,
);
b.draw_chart(
Rect::new(chart_x, chart_y, chart_w, chart_h),
&chart,
Some((0, 0)),
None,
);
});
backend.end_frame();
let (r, g, b, _) = surface.pixel(5, 5);
assert_eq!(
(r, g, b),
(sentinel.r, sentinel.g, sentinel.b),
"hover marker must not paint outside the chart's own rect",
);
}
#[test]
fn mac_backend_chart_axis_label_space_is_reserved_in_chrome_metrics() {
const CW: u32 = 320;
const CH: u32 = 200;
let chart = ChartModel {
id: WidgetId::new("ch"),
kind: ChartKind::Line,
series: vec![Series {
label: "SER".into(),
data: vec![0.0, 20.0, 40.0, 60.0, 80.0],
color: None,
fill: false,
}],
x_label: None,
y_label: None,
y_range: Some((0.0, 80.0)),
x_range: Some((1000.0, 3000.0)),
show_legend: true,
y_ticks: Some(2),
x_ticks: Some(2),
show_grid: false,
};
let surface = BitmapSurface::new(CW, CH);
surface.fill(0.0, 0.0, 0.0, 0.0);
let mut backend = MacBackend::new();
backend.set_current_font(super::super::text::make_font("Menlo", 5.0).expect("Menlo"));
backend.set_chrome_font(super::super::text::make_font("Menlo", 24.0).expect("Menlo"));
assert!(
backend.chrome_char_width - backend.current_char_width > 1.0
&& backend.chrome_line_height - backend.current_line_height > 1.0,
"chrome and editor metrics must differ enough that a layout built from the \
editor's can't pass by coincidence: chrome=(cw {}, lh {}), editor=(cw {}, lh {})",
backend.chrome_char_width,
backend.chrome_line_height,
backend.current_char_width,
backend.current_line_height,
);
backend.set_painted_text_recording(true);
backend.begin_frame(Viewport::new(CW as f32, CH as f32, 1.0));
let layout_cell = std::cell::RefCell::new(None);
backend.enter_frame_scope(surface.context_ptr(), |b| {
let l = b.draw_chart(
Rect::new(0.0, 0.0, CW as f32, CH as f32),
&chart,
None,
None,
);
*layout_cell.borrow_mut() = Some(l);
});
backend.end_frame();
let layout = layout_cell.into_inner().expect("draw_chart ran");
let runs = backend.text_runs();
let run_for = |label: &str| {
runs.iter()
.find(|r| r.text == label)
.unwrap_or_else(|| {
panic!(
"expected a painted run for the axis label {label:?}; painted: {:?}",
runs.iter().map(|r| r.text.as_str()).collect::<Vec<_>>()
)
})
.bounds
};
let gutter = layout.plot_area.x - layout.bounds.x;
let chrome_gutter = backend.chrome_char_width as f32 * 3.0;
let editor_gutter = backend.current_char_width as f32 * 3.0;
assert!(
(gutter - chrome_gutter).abs() < 0.01,
"the y-axis gutter must be sized in chrome_char_width, the advance the \
tick labels actually paint in: got {gutter}, expected {chrome_gutter} \
(the editor advance would have reserved {editor_gutter})",
);
assert!(
!layout.y_tick_positions.is_empty(),
"y_ticks: Some(2) must produce tick positions to label",
);
for &(_, val) in &layout.y_tick_positions {
let label = crate::primitives::chart::format_tick_value(val);
let rb = run_for(&label);
assert!(
rb.x >= layout.bounds.x - 0.5,
"y-tick label {label:?} must fit inside the reserved gutter, not spill \
past the chart's left edge: run x={}, chart x={}",
rb.x,
layout.bounds.x,
);
assert!(
rb.x + rb.width <= layout.plot_area.x + 0.5,
"y-tick label {label:?} must stay left of the plot area it labels: run \
ends at {}, plot area starts at {}",
rb.x + rb.width,
layout.plot_area.x,
);
}
assert!(
!layout.x_tick_positions.is_empty(),
"x_ticks: Some(2) must produce tick positions to label",
);
let chart_bottom = layout.bounds.y + layout.bounds.height;
for &(_, val) in &layout.x_tick_positions {
let label = crate::primitives::chart::format_tick_value(val);
let rb = run_for(&label);
assert!(
rb.y + rb.height <= chart_bottom + 1.0,
"x-tick label {label:?} must fit the reserved tick row inside the chart's \
own bounds: run bottom={}, chart bottom={chart_bottom}",
rb.y + rb.height,
);
}
let lb = layout
.legend_bounds
.expect("show_legend: true must reserve a legend row");
assert!(
(lb.height - backend.chrome_line_height as f32).abs() < 0.01,
"the legend row must be one chrome line height tall: got {}, chrome={}, \
editor={}",
lb.height,
backend.chrome_line_height,
backend.current_line_height,
);
let legend_run = run_for("SER");
assert!(
legend_run.y + legend_run.height <= lb.y + lb.height + 1.0,
"the legend label must fit the reserved legend row: run bottom={}, row \
bottom={}",
legend_run.y + legend_run.height,
lb.y + lb.height,
);
}
#[test]
fn mac_backend_draw_status_bar_zero_width_does_not_paint() {
use super::super::headless::BitmapSurface;
use crate::primitives::status_bar::{StatusBar, StatusBarSegment};
use crate::types::Color;
const W: u32 = 40;
const H: u32 = 40;
let sentinel = Color::rgb(7, 8, 9);
let surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.surface_fill_rect(Rect::new(0.0, 0.0, W as f32, H as f32), sentinel);
b.draw_status_bar_interactive(
Rect::new(10.0, 10.0, 0.0, 15.0),
&StatusBar {
id: WidgetId::new("test:status-bar"),
left_segments: vec![StatusBarSegment {
text: "READY".into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(200, 0, 0),
bold: false,
action_id: None,
}],
right_segments: vec![],
},
&crate::InteractionState::new(),
);
});
backend.end_frame();
for y in 0..H {
for x in 0..W {
let (r, g, b, _) = surface.pixel(x, y);
assert_eq!(
(r, g, b),
(sentinel.r, sentinel.g, sentinel.b),
"pixel ({x}, {y}) must stay untouched: a zero-width rect must \
short-circuit to the no-paint layout",
);
}
}
}
use crate::primitives::text_display::{
TextDisplay as TextDisplayModel, TextDisplayHit, TextDisplayLine as TextDisplayLineModel,
};
use crate::types::Decoration;
const TD_W: u32 = 240;
const TD_H: u32 = 160;
fn td_line(text: &str) -> TextDisplayLineModel {
TextDisplayLineModel {
spans: vec![crate::types::StyledSpan::plain(text)],
decoration: Decoration::Normal,
timestamp: None,
}
}
fn make_td(lines: usize, show_scrollbar: bool) -> TextDisplayModel {
TextDisplayModel {
id: WidgetId::new("td"),
lines: (0..lines).map(|i| td_line(&format!("ln{i}"))).collect(),
scroll_offset: 0,
auto_scroll: false,
max_lines: 0,
has_focus: false,
title: None,
show_scrollbar,
}
}
fn paint_td_via_backend(
td: &TextDisplayModel,
rect: Rect,
) -> (
BitmapSurface,
crate::primitives::text_display::TextDisplayLayout,
) {
let surface = BitmapSurface::new(TD_W, TD_H);
surface.fill(0.0, 0.0, 0.0, 0.0);
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.begin_frame(Viewport::new(TD_W as f32, TD_H as f32, 1.0));
let layout = std::cell::RefCell::new(None);
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.draw_text_display(rect, td);
let l = b.text_display_layout(rect, td);
*layout.borrow_mut() = Some(l);
});
backend.end_frame();
(surface, layout.into_inner().unwrap())
}
#[test]
fn mac_backend_draw_text_display_background_fills_theme_background() {
let td = make_td(0, false);
let (s, _) = paint_td_via_backend(&td, Rect::new(0.0, 0.0, TD_W as f32, TD_H as f32));
let theme = Theme::default();
let (r, g, b, _) = s.pixel(TD_W / 2, TD_H / 2);
assert_eq!(
(r, g, b),
(theme.background.r, theme.background.g, theme.background.b),
);
}
#[test]
fn mac_backend_draw_text_display_scrollbar_gutter_paints_track_colour() {
let td = make_td(100, true);
let (s, layout) = paint_td_via_backend(&td, Rect::new(0.0, 0.0, TD_W as f32, TD_H as f32));
let gutter = layout.scrollbar_bounds.expect("gutter present");
let probe_x = (gutter.x + gutter.width / 2.0) as u32;
let probe_y = (gutter.y + gutter.height - 2.0) as u32;
let (r, g, b, _) = s.pixel(probe_x, probe_y);
let theme = Theme::default();
assert_eq!(
(r, g, b),
(
theme.scrollbar_track.r,
theme.scrollbar_track.g,
theme.scrollbar_track.b,
),
);
}
#[test]
fn mac_backend_draw_text_display_scrollbar_thumb_paints_thumb_colour() {
let td = make_td(100, true);
let (s, layout) = paint_td_via_backend(&td, Rect::new(0.0, 0.0, TD_W as f32, TD_H as f32));
let thumb = layout.thumb_bounds.expect("thumb present");
let probe_x = (thumb.x + thumb.width / 2.0) as u32;
let probe_y = (thumb.y + thumb.height / 2.0) as u32;
let (r, g, b, _) = s.pixel(probe_x, probe_y);
let theme = Theme::default();
assert_eq!(
(r, g, b),
(
theme.scrollbar_thumb.r,
theme.scrollbar_thumb.g,
theme.scrollbar_thumb.b,
),
);
}
#[test]
fn mac_backend_draw_text_display_layout_hit_test_resolves_lines() {
let td = make_td(20, false);
let (_, layout) = paint_td_via_backend(&td, Rect::new(0.0, 0.0, TD_W as f32, TD_H as f32));
let vis = &layout.visible_lines[0];
let cx = vis.bounds.x + vis.bounds.width / 2.0;
let cy = vis.bounds.y + vis.bounds.height / 2.0;
match layout.hit_test(cx, cy) {
TextDisplayHit::Line(idx) => assert_eq!(idx, vis.line_idx),
other => panic!("expected Line, got {:?}", other),
}
}
#[test]
fn mac_backend_draw_text_display_layout_hit_test_resolves_lines_at_nonzero_origin() {
let mut td = make_td(20, false);
td.title = Some(crate::types::StyledText::plain("Logs"));
let rect_x = 9.0_f32;
let rect_y = 17.0_f32;
let rect = Rect::new(rect_x, rect_y, TD_W as f32 - rect_x, TD_H as f32 - rect_y);
let (_surface, layout) = paint_td_via_backend(&td, rect);
let line_height = 16.0_f32;
let body_y = rect_y + line_height;
let vis = &layout.visible_lines[0];
assert_eq!(
vis.bounds.y, 0.0,
"visible_lines bounds.y must be body-local, got {}",
vis.bounds.y,
);
let abs_x = rect_x + vis.bounds.x + vis.bounds.width * 0.5;
let abs_y = body_y + vis.bounds.y + vis.bounds.height * 0.5;
let local_x = abs_x - rect_x;
let local_y = abs_y - body_y;
match layout.hit_test(local_x, local_y) {
TextDisplayHit::Line(idx) => assert_eq!(idx, vis.line_idx),
other => panic!("expected Line, got {:?}", other),
}
}
#[test]
fn chrome_font_defaults_to_system_ui_font_not_monospace() {
let b = MacBackend::new();
let expected = super::super::text::system_ui_font(DEFAULT_UI_FONT_SIZE_PT);
assert_eq!(
b.chrome_font.family_name(),
expected.family_name(),
"an untouched MacBackend's chrome_font must be the CoreText system UI font"
);
assert_ne!(
b.chrome_font.family_name(),
"Menlo",
"the default chrome font must not be a monospace editor face"
);
}
#[test]
fn set_editor_font_and_set_ui_font_are_independent() {
use crate::Backend;
let mut b = MacBackend::new();
b.set_editor_font("Menlo", 14.0);
let editor_line_height_before = b.current_line_height;
let chrome_line_height_before = b.chrome_line_height;
Backend::set_ui_font(&mut b, "Helvetica 40");
assert_eq!(
b.current_line_height, editor_line_height_before,
"set_ui_font must not touch the editor font's cached line height"
);
assert!(
b.chrome_line_height > chrome_line_height_before,
"set_ui_font(\"Helvetica 40\") must grow chrome_line_height from the \
{DEFAULT_UI_FONT_SIZE_PT}pt default: before={chrome_line_height_before}, \
after={}",
b.chrome_line_height,
);
assert_eq!(b.chrome_font.family_name(), "Helvetica");
assert_eq!(b.chrome_font.pt_size(), 40.0);
let chrome_line_height_after_ui = b.chrome_line_height;
Backend::set_editor_font(&mut b, "Menlo", 40.0);
assert_eq!(
b.chrome_line_height, chrome_line_height_after_ui,
"set_editor_font must not touch chrome_line_height"
);
assert_eq!(
b.current_font
.as_ref()
.expect("set_current_font ran")
.family_name(),
"Menlo",
);
}
#[test]
fn measure_text_resolves_a_different_font_per_role() {
use crate::{Backend, FontRole};
let mut b = MacBackend::new();
b.set_editor_font("Menlo", 10.0);
Backend::set_ui_font(&mut b, "Helvetica 40");
let (chrome_w, _) = Backend::measure_text(&b, "quadraui", FontRole::Chrome);
let (editor_w, _) = Backend::measure_text(&b, "quadraui", FontRole::Editor);
assert!(
chrome_w > editor_w * 2.0,
"a 40pt chrome font should measure \"quadraui\" much wider than a \
10pt editor font: chrome_w={chrome_w}, editor_w={editor_w}"
);
}
#[test]
fn measure_text_editor_role_falls_back_to_chrome_font_before_set_current_font() {
use crate::{Backend, FontRole};
let b = MacBackend::new();
let (w, h) = Backend::measure_text(&b, "quadraui", FontRole::Editor);
assert!(w > 0.0, "expected a real measurement, got width {w}");
assert!(h > 0.0, "expected a real measurement, got height {h}");
}
#[test]
fn mac_backend_default_fonts_reports_platform_convention() {
use crate::Backend;
let b = MacBackend::new();
let defaults = Backend::default_fonts(&b);
assert_eq!(defaults.editor_family, "Menlo");
assert_eq!(defaults.editor_size_pt, 12.0);
assert_eq!(
defaults.ui_family,
super::super::text::system_ui_font(13.0).family_name()
);
assert_eq!(defaults.ui_size_pt, 13.0);
}
#[test]
fn mac_backend_default_fonts_is_unaffected_by_set_editor_font_and_set_ui_font() {
use crate::Backend;
let mut b = MacBackend::new();
b.set_editor_font("Helvetica", 40.0);
Backend::set_ui_font(&mut b, "Helvetica 40");
let defaults = Backend::default_fonts(&b);
assert_eq!(defaults.editor_family, "Menlo");
assert_eq!(defaults.editor_size_pt, 12.0);
assert_eq!(
defaults.ui_family,
super::super::text::system_ui_font(13.0).family_name(),
"ui_family is computed via a live system_ui_font() call, not a \
literal — it's the field most worth re-checking after a prior \
set_editor_font/set_ui_font override"
);
assert_eq!(defaults.ui_size_pt, 13.0);
}
#[test]
fn set_ui_font_degrades_to_system_font_on_unparseable_or_unknown_family() {
use crate::Backend;
let mut b = MacBackend::new();
Backend::set_ui_font(&mut b, "not-a-valid-description");
assert_eq!(
b.chrome_font.family_name(),
super::super::text::system_ui_font(DEFAULT_UI_FONT_SIZE_PT).family_name(),
"an unparseable font_desc must degrade to the system UI font"
);
Backend::set_ui_font(&mut b, "Definitely Not A Real Font Family 12");
assert_eq!(
b.chrome_font.family_name(),
super::super::text::system_ui_font(12.0).family_name(),
"an unknown family must degrade to the system UI font at the requested size"
);
assert_eq!(b.chrome_font.pt_size(), 12.0);
}
#[test]
fn set_editor_font_keeps_the_installed_font_for_an_unknown_family() {
use crate::Backend;
let mut b = MacBackend::new();
Backend::set_editor_font(&mut b, "Menlo", 14.0);
let line_height_before = b.current_line_height;
Backend::set_editor_font(&mut b, "Definitely Not A Real Font Family", 30.0);
assert_eq!(
b.current_font
.as_ref()
.expect("editor font still installed")
.family_name(),
"Menlo",
"an unknown family must not replace the installed editor font",
);
assert_eq!(
b.current_line_height, line_height_before,
"a rejected set_editor_font must not move the cached line height",
);
}
#[test]
fn set_editor_font_resolves_the_monospace_generic_token() {
use crate::Backend;
let mut b = MacBackend::new();
Backend::set_editor_font(&mut b, "monospace", 14.0);
let expected = super::super::text::system_monospace_font(14.0);
assert_eq!(
b.current_font
.as_ref()
.expect("set_current_font ran")
.family_name(),
expected.family_name(),
"\"monospace\" must resolve to the CoreText fixed-pitch system font"
);
}
#[test]
fn set_ui_font_resolves_generic_tokens() {
use crate::Backend;
let mut b = MacBackend::new();
Backend::set_ui_font(&mut b, "monospace 13");
let expected_mono = super::super::text::system_monospace_font(13.0);
assert_eq!(b.chrome_font.family_name(), expected_mono.family_name());
assert_eq!(b.chrome_font.pt_size(), 13.0);
Backend::set_ui_font(&mut b, "sans-serif 15");
let expected_ui = super::super::text::system_ui_font(15.0);
assert_eq!(b.chrome_font.family_name(), expected_ui.family_name());
assert_eq!(b.chrome_font.pt_size(), 15.0);
Backend::set_ui_font(&mut b, "system-ui 17");
assert_eq!(b.chrome_font.family_name(), expected_ui.family_name());
assert_eq!(b.chrome_font.pt_size(), 17.0);
Backend::set_ui_font(&mut b, "Monospace 19");
assert_eq!(
b.chrome_font.family_name(),
super::super::text::system_monospace_font(19.0).family_name()
);
Backend::set_ui_font(&mut b, "Sans 21");
assert_eq!(
b.chrome_font.family_name(),
super::super::text::system_ui_font(21.0).family_name()
);
}
#[test]
fn set_ui_font_resolves_the_first_installed_family_in_a_comma_list() {
use crate::Backend;
let mut b = MacBackend::new();
Backend::set_ui_font(
&mut b,
"Definitely Not A Real Font Family One, Definitely Not A Real Font Family Two, \
Menlo 16",
);
assert_eq!(b.chrome_font.family_name(), "Menlo");
assert_eq!(b.chrome_font.pt_size(), 16.0);
}
#[test]
fn set_ui_font_resolves_a_comma_list_with_no_trailing_size() {
use crate::Backend;
let mut b = MacBackend::new();
Backend::set_ui_font(
&mut b,
"Definitely Not A Real Font Family One, Definitely Not A Real Font Family Two, Menlo",
);
assert_eq!(b.chrome_font.family_name(), "Menlo");
assert_eq!(b.chrome_font.pt_size(), DEFAULT_UI_FONT_SIZE_PT);
}
#[test]
fn set_ui_font_resolves_a_generic_token_later_in_a_comma_list() {
use crate::Backend;
let mut b = MacBackend::new();
Backend::set_ui_font(
&mut b,
"Definitely Not A Real Font Family One, Definitely Not A Real Font Family Two, \
sans-serif",
);
assert_eq!(
b.chrome_font.family_name(),
super::super::text::system_ui_font(DEFAULT_UI_FONT_SIZE_PT).family_name()
);
}
#[test]
fn draw_status_bar_interactive_paints_with_chrome_font_not_editor_font() {
use super::super::headless::BitmapSurface;
use crate::primitives::status_bar::{StatusBar, StatusBarSegment};
use crate::Backend;
const W: u32 = 600;
const H: u32 = 40;
fn painted_segment_width(chrome_desc: &str, editor: (&str, f32)) -> f32 {
let surface = BitmapSurface::new(W, H);
let mut b = MacBackend::new();
Backend::set_editor_font(&mut b, editor.0, editor.1);
Backend::set_ui_font(&mut b, chrome_desc);
b.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let bar = StatusBar {
id: WidgetId::new("status"),
left_segments: vec![StatusBarSegment {
text: "Save".into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(10, 10, 10),
bold: false,
action_id: None,
}],
right_segments: vec![],
};
let layout = std::cell::RefCell::new(None);
b.enter_frame_scope(surface.context_ptr(), |backend| {
let l = backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&bar,
&crate::InteractionState::new(),
);
*layout.borrow_mut() = Some(l);
});
b.end_frame();
layout.into_inner().unwrap().visible_segments[0]
.bounds
.width
}
let w_small_editor = painted_segment_width("Menlo 11", ("Menlo", 10.0));
let w_huge_editor = painted_segment_width("Menlo 11", ("Menlo", 80.0));
assert!(
(w_small_editor - w_huge_editor).abs() < 0.01,
"status bar width must be unaffected by set_editor_font: {w_small_editor} vs \
{w_huge_editor}"
);
let w_small_chrome = painted_segment_width("Menlo 8", ("Menlo", 14.0));
let w_huge_chrome = painted_segment_width("Menlo 60", ("Menlo", 14.0));
assert!(
w_huge_chrome > w_small_chrome * 2.0,
"status bar width must grow with set_ui_font: {w_small_chrome} vs {w_huge_chrome}"
);
}
#[test]
fn draw_status_bar_interactive_scaled_shrinks_without_perturbing_later_unscaled_paints() {
use super::super::headless::BitmapSurface;
use crate::primitives::status_bar::{StatusBar, StatusBarSegment};
use crate::Backend;
const W: u32 = 600;
const H: u32 = 40;
fn sample_bar() -> StatusBar {
StatusBar {
id: WidgetId::new("status"),
left_segments: vec![StatusBarSegment {
text: "Save".into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(10, 10, 10),
bold: false,
action_id: None,
}],
right_segments: vec![],
}
}
let surface = BitmapSurface::new(W, H);
let mut b = MacBackend::new();
Backend::set_ui_font(&mut b, "Menlo 40");
b.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let bar = sample_bar();
let unscaled_w = std::cell::RefCell::new(0.0f32);
let scaled_w = std::cell::RefCell::new(0.0f32);
let unscaled_w_again = std::cell::RefCell::new(0.0f32);
b.enter_frame_scope(surface.context_ptr(), |backend| {
let l1 = backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&bar,
&crate::InteractionState::new(),
);
*unscaled_w.borrow_mut() = l1.visible_segments[0].bounds.width;
let l2 = backend.draw_status_bar_interactive_scaled(
Rect::new(0.0, 0.0, W as f32, H as f32),
&bar,
&crate::InteractionState::new(),
0.5,
);
*scaled_w.borrow_mut() = l2.visible_segments[0].bounds.width;
let l3 = backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&bar,
&crate::InteractionState::new(),
);
*unscaled_w_again.borrow_mut() = l3.visible_segments[0].bounds.width;
});
b.end_frame();
let unscaled_w = unscaled_w.into_inner();
let scaled_w = scaled_w.into_inner();
let unscaled_w_again = unscaled_w_again.into_inner();
assert!(
scaled_w < unscaled_w * 0.75,
"font_scale=0.5 should paint noticeably narrower: {scaled_w} vs {unscaled_w}",
);
assert!(
(unscaled_w_again - unscaled_w).abs() < 0.01,
"an unscaled paint after a scaled one must match the original unscaled width \
exactly — chrome font state must be restored, not left scaled: \
{unscaled_w_again} vs {unscaled_w}",
);
}
#[test]
fn status_bar_layout_matches_painted_layout_after_set_ui_font() {
use super::super::headless::BitmapSurface;
use crate::primitives::status_bar::{StatusBar, StatusBarSegment};
use crate::Backend;
const W: u32 = 300;
const H: u32 = 30;
let mut b = MacBackend::new();
Backend::set_editor_font(&mut b, "Menlo", 14.0);
Backend::set_ui_font(&mut b, "Helvetica 22");
b.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let bar = StatusBar {
id: WidgetId::new("status"),
left_segments: vec![StatusBarSegment {
text: "Save".into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(10, 10, 10),
bold: false,
action_id: None,
}],
right_segments: vec![],
};
let surface = BitmapSurface::new(W, H);
let painted = std::cell::RefCell::new(None);
b.enter_frame_scope(surface.context_ptr(), |backend| {
let l = backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&bar,
&crate::InteractionState::new(),
);
*painted.borrow_mut() = Some(l);
});
b.end_frame();
let painted = painted.into_inner().unwrap();
let computed = b.status_bar_layout(Rect::new(0.0, 0.0, W as f32, H as f32), &bar);
assert_eq!(
painted.visible_segments.len(),
computed.visible_segments.len(),
);
for (p, c) in painted
.visible_segments
.iter()
.zip(computed.visible_segments.iter())
{
assert!((p.bounds.x - c.bounds.x).abs() < 0.001);
assert!((p.bounds.width - c.bounds.width).abs() < 0.001);
}
}
#[test]
fn draw_status_bar_interactive_fills_the_whole_rect_height_not_just_line_height() {
use super::super::headless::BitmapSurface;
use crate::primitives::status_bar::StatusBar;
use crate::theme::Theme;
use crate::types::Color;
const W: u32 = 200;
const H: u32 = 40;
let theme = Theme {
background: Color::rgb(236, 236, 236),
..Theme::default()
};
let surface = BitmapSurface::new(W, H);
let mut b = MacBackend::new();
b.set_current_theme(theme);
Backend::set_ui_font(&mut b, "Menlo 8");
assert!(
(b.chrome_line_height as u32) < H,
"test setup: chrome_line_height ({}) must be smaller than the rect height \
({H}) for this to exercise the bug",
b.chrome_line_height,
);
b.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let bar = StatusBar {
id: WidgetId::new("status"),
left_segments: vec![],
right_segments: vec![],
};
b.enter_frame_scope(surface.context_ptr(), |backend| {
let _ = backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&bar,
&crate::InteractionState::new(),
);
});
b.end_frame();
let (r, g, bl, _a) = surface.pixel(W / 2, H - 2);
assert_eq!(
(r, g, bl),
(theme.background.r, theme.background.g, theme.background.b),
"the bottom of a status-bar rect taller than chrome_line_height must be \
filled with the theme background, not left unpainted: got ({r}, {g}, {bl})"
);
}
#[test]
fn draw_tree_uses_ui_font_not_editor_font() {
use super::super::headless::BitmapSurface;
use crate::types::{Decoration, SelectionMode, StyledText, TreeStyle};
const W: u32 = 800;
const H: u32 = 120;
const PINNED_LINE_HEIGHT: f64 = 60.0;
const PINNED_CHAR_WIDTH: f64 = 8.0;
fn row_text_extent(editor: (&str, f32), ui_font: Option<&str>) -> u32 {
let surface = BitmapSurface::new(W, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
let mut b = MacBackend::new();
Backend::set_editor_font(&mut b, editor.0, editor.1);
b.set_current_line_height(PINNED_LINE_HEIGHT);
b.set_current_char_width(PINNED_CHAR_WIDTH);
if let Some(f) = ui_font {
Backend::set_ui_font(&mut b, f);
}
b.set_current_theme(crate::Theme {
tab_bar_bg: Color::rgb(255, 255, 255),
background: Color::rgb(255, 255, 255),
foreground: Color::rgb(0, 0, 0),
..crate::Theme::default()
});
b.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let tree = TreeView {
id: WidgetId::new("test:tree"),
rows: vec![crate::primitives::tree::TreeRow {
path: vec![0],
indent: 0,
icon: None,
text: StyledText::plain("m".to_string()),
badge: None,
is_expanded: None,
decoration: Decoration::Normal,
edit: None,
}],
selection_mode: SelectionMode::Single,
selected_path: None,
scroll_offset: 0,
style: TreeStyle::default(),
has_focus: false,
};
b.enter_frame_scope(surface.context_ptr(), |backend| {
backend.draw_tree(Rect::new(0.0, 0.0, W as f32, H as f32), &tree);
});
b.end_frame();
(0..W)
.rev()
.find(|&x| (0..H).any(|y| surface.pixel(x, y) != (255, 255, 255, 255)))
.unwrap_or(0)
}
let small_editor_extent = row_text_extent(("Menlo", 8.0), None);
let large_editor_extent = row_text_extent(("Menlo", 60.0), None);
assert!(
small_editor_extent.abs_diff(large_editor_extent) <= 1,
"tree row glyph extent must be editor-font-size independent: \
small_editor={small_editor_extent}, large_editor={large_editor_extent}"
);
let ui_font_extent = row_text_extent(("Menlo", 8.0), Some("Helvetica 60"));
assert!(
ui_font_extent > small_editor_extent + 20,
"changing ui_font alone must visibly widen the painted row label: \
default_ui_font={small_editor_extent}, ui_font_Helvetica_60={ui_font_extent}"
);
}
fn flat_tree(n_rows: usize) -> TreeView {
TreeView {
id: WidgetId::new("test:tree:vscrollbar"),
rows: (0..n_rows)
.map(|i| crate::primitives::tree::TreeRow {
path: vec![i as u16],
indent: 0,
icon: None,
text: crate::types::StyledText::plain(format!("row{i}")),
badge: None,
is_expanded: None,
decoration: crate::types::Decoration::Normal,
edit: None,
})
.collect(),
selection_mode: crate::types::SelectionMode::Single,
selected_path: None,
scroll_offset: 0,
style: crate::types::TreeStyle::default(),
has_focus: false,
}
}
#[test]
fn mac_backend_tree_vscrollbar_uses_tree_layout_row_pitch_not_raw_line_height() {
let backend = MacBackend::new();
let row_pitch = (backend.chrome_line_height * 1.4).round();
let n_rows = ((250.0 / row_pitch).ceil() as usize) + 2;
let tree = flat_tree(n_rows);
let rect = Rect::new(0.0, 0.0, 20.0, 250.0);
let sb = Backend::tree_vscrollbar(&backend, rect, &tree)
.expect("rows at chrome_line_height row pitch must overflow a 250px viewport");
let expected_row_h = row_pitch as f32;
assert_eq!(
sb.track.width,
crate::primitives::layout_metrics::pixel::TREE_VSCROLLBAR_WIDTH,
"track width must stay the fixed VS Code list/tree scrollbar width"
);
assert!(
sb.thumb_len >= expected_row_h,
"minimum thumb length (and row pitch) must match tree_layout's \
non-header item_height, not the raw line_height"
);
}
#[test]
fn mac_backend_tree_vscrollbar_honors_tree_style_row_height_override() {
let backend = MacBackend::new();
let mut tree = flat_tree(15);
tree.style.row_height = Some(50);
let rect = Rect::new(0.0, 0.0, 20.0, 300.0);
let sb = Backend::tree_vscrollbar(&backend, rect, &tree)
.expect("15 rows at a 50px override overflow a 300px viewport (6 visible)");
assert_eq!(
sb.track.width,
crate::primitives::layout_metrics::pixel::TREE_VSCROLLBAR_WIDTH
);
assert!(sb.thumb_len >= 50.0);
}
#[test]
fn draw_menu_bar_uses_ui_font_not_editor_font() {
use super::super::headless::BitmapSurface;
const W: u32 = 800;
const H: u32 = 30;
fn item_width(editor: (&str, f32), ui_font: &str) -> f32 {
let surface = BitmapSurface::new(W, H);
let mut b = MacBackend::new();
Backend::set_editor_font(&mut b, editor.0, editor.1);
Backend::set_ui_font(&mut b, ui_font);
Backend::set_menu_style(&mut b, crate::backend::MenuStyle::Custom);
b.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let bar = MenuBar {
id: WidgetId::new("test:menu-bar"),
items: vec![crate::MenuBarItem {
id: WidgetId::new("test:menu-bar:file"),
label: "&File".to_string(),
disabled: false,
submenu: None,
}],
open_item: None,
focused_item: None,
};
let width = std::cell::RefCell::new(0.0f32);
b.enter_frame_scope(surface.context_ptr(), |backend| {
let layout = backend.draw_menu_bar(Rect::new(0.0, 0.0, W as f32, H as f32), &bar);
*width.borrow_mut() = layout.visible_items[0].bounds.width;
});
b.end_frame();
width.into_inner()
}
let w_small_editor = item_width(("Menlo", 10.0), "Helvetica 11");
let w_huge_editor = item_width(("Menlo", 80.0), "Helvetica 11");
assert!(
(w_small_editor - w_huge_editor).abs() < 0.5,
"menu bar item width must be unaffected by set_editor_font: \
{w_small_editor} vs {w_huge_editor}"
);
let w_small_chrome = item_width(("Menlo", 14.0), "Helvetica 8");
let w_huge_chrome = item_width(("Menlo", 14.0), "Helvetica 60");
assert!(
w_huge_chrome > w_small_chrome * 2.0,
"menu bar item width must grow with set_ui_font: {w_small_chrome} vs {w_huge_chrome}"
);
}
fn sample_menu_bar_for_native_default_tests() -> MenuBar {
MenuBar {
id: WidgetId::new("test:menu-bar"),
items: vec![crate::MenuBarItem {
id: WidgetId::new("test:menu-bar:file"),
label: "&File".to_string(),
disabled: false,
submenu: None,
}],
open_item: None,
focused_item: None,
}
}
#[test]
fn draw_menu_bar_defaults_to_native_and_zeroes_height_on_auto() {
use super::super::headless::BitmapSurface;
const W: u32 = 300;
const H: u32 = 24;
let surface = BitmapSurface::new(W, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
let bar = sample_menu_bar_for_native_default_tests();
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
let mut backend = MacBackend::new();
assert_eq!(backend.menu_style(), crate::backend::MenuStyle::Auto);
let layout = std::cell::RefCell::new(None);
backend.enter_frame_scope(surface.context_ptr(), |b| {
*layout.borrow_mut() = Some(b.draw_menu_bar(rect, &bar));
});
let layout = layout
.into_inner()
.expect("draw_menu_bar ran inside the frame scope");
assert_eq!(
layout.bounds.height, 0.0,
"native path must reserve no vertical space for an in-window strip"
);
assert_eq!(layout.bounds.width, rect.width);
assert!(layout.visible_items.is_empty());
assert!(layout.hit_regions.is_empty());
let painted_any = (0..W).any(|x| {
(0..H).any(|y| {
let (r, g, b, _) = surface.pixel(x, y);
(r, g, b) != (255, 255, 255)
})
});
assert!(
!painted_any,
"native path must not paint the in-window strip at all"
);
}
#[test]
fn menu_bar_layout_agrees_with_draw_menu_bar_under_each_menu_style() {
use super::super::headless::BitmapSurface;
const W: u32 = 300;
const H: u32 = 24;
let bar = sample_menu_bar_for_native_default_tests();
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
for style in [
crate::backend::MenuStyle::Auto,
crate::backend::MenuStyle::Custom,
] {
let surface = BitmapSurface::new(W, H);
let mut backend = MacBackend::new();
backend.set_menu_style(style);
let no_paint = backend.menu_bar_layout(rect, &bar);
let painted =
backend.enter_frame_scope(surface.context_ptr(), |b| b.draw_menu_bar(rect, &bar));
assert_eq!(no_paint, painted, "{style:?}");
if style == crate::backend::MenuStyle::Custom {
assert!(
!no_paint.visible_items.is_empty(),
"the painted opt-out lays out its items"
);
}
}
}
#[test]
fn draw_menu_bar_falls_back_to_painted_strip_on_custom() {
use super::super::headless::BitmapSurface;
const W: u32 = 300;
const H: u32 = 24;
let bar = sample_menu_bar_for_native_default_tests();
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
let direct_surface = BitmapSurface::new(W, H);
direct_surface.fill(1.0, 1.0, 1.0, 1.0);
let mut direct_backend = MacBackend::new();
let direct_layout = std::cell::RefCell::new(None);
direct_backend.enter_frame_scope(direct_surface.context_ptr(), |b| {
*direct_layout.borrow_mut() = Some(b.draw_menu_bar_painted(rect, &bar));
});
let direct_layout = direct_layout.into_inner().unwrap();
let surface = BitmapSurface::new(W, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
let mut backend = MacBackend::new();
backend.set_menu_style(crate::backend::MenuStyle::Custom);
let layout = std::cell::RefCell::new(None);
backend.enter_frame_scope(surface.context_ptr(), |b| {
*layout.borrow_mut() = Some(b.draw_menu_bar(rect, &bar));
});
let layout = layout.into_inner().unwrap();
assert_eq!(layout, direct_layout);
assert!(
layout.bounds.height > 0.0,
"the painted strip must reserve real vertical space"
);
for y in 0..H {
for x in 0..W {
assert_eq!(
surface.pixel(x, y),
direct_surface.pixel(x, y),
"pixel ({x}, {y}) must match a direct draw_menu_bar_painted call once opted out",
);
}
}
}
#[test]
fn install_menu_bar_if_changed_skips_reinstall_for_an_unchanged_bar() {
let bar = sample_menu_bar_for_native_default_tests();
let mut backend = MacBackend::new();
assert!(backend.last_native_menu_bar.is_none());
assert!(
backend.install_menu_bar_if_changed(&bar),
"the first install of a bar must build the NSMenu tree",
);
assert_eq!(backend.last_native_menu_bar.as_ref(), Some(&bar));
assert!(
!backend.install_menu_bar_if_changed(&bar),
"an unchanged bar must not rebuild the NSMenu tree",
);
assert_eq!(backend.last_native_menu_bar.as_ref(), Some(&bar));
let mut other = bar.clone();
other.items[0].label = "&Edit".to_string();
assert!(
backend.install_menu_bar_if_changed(&other),
"a changed bar must rebuild the NSMenu tree",
);
assert_eq!(backend.last_native_menu_bar.as_ref(), Some(&other));
}
#[test]
fn draw_activity_bar_uses_ui_font_not_editor_font() {
use super::super::headless::BitmapSurface;
const W: u32 = 200;
const H: u32 = 60;
fn icon_extent(editor: (&str, f32), ui_font: Option<&str>) -> u32 {
let surface = BitmapSurface::new(W, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
let mut b = MacBackend::new();
Backend::set_editor_font(&mut b, editor.0, editor.1);
if let Some(f) = ui_font {
Backend::set_ui_font(&mut b, f);
}
b.set_current_theme(crate::Theme {
tab_bar_bg: Color::rgb(255, 255, 255),
background: Color::rgb(255, 255, 255),
foreground: Color::rgb(0, 0, 0),
inactive_fg: Color::rgb(0, 0, 0),
..crate::Theme::default()
});
b.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let bar = crate::ActivityBar {
id: WidgetId::new("test:activity-bar"),
top_items: vec![crate::ActivityItem {
id: WidgetId::new("test:activity:item"),
icon: crate::types::Icon::new("x", "WWWW"),
tooltip: String::new(),
is_active: false,
is_keyboard_selected: false,
}],
bottom_items: vec![],
active_accent: None,
selection_bg: None,
is_keyboard_focused: false,
};
b.enter_frame_scope(surface.context_ptr(), |backend| {
backend.draw_activity_bar(Rect::new(0.0, 0.0, W as f32, H as f32), &bar, None);
});
b.end_frame();
let mut min_x: Option<u32> = None;
let mut max_x: Option<u32> = None;
for x in 0..W {
for y in 0..H {
if surface.pixel(x, y) != (255, 255, 255, 255) {
min_x = Some(min_x.map_or(x, |m| m.min(x)));
max_x = Some(max_x.map_or(x, |m| m.max(x)));
}
}
}
match (min_x, max_x) {
(Some(lo), Some(hi)) => hi - lo,
_ => 0,
}
}
let small_editor_extent = icon_extent(("Menlo", 8.0), None);
let large_editor_extent = icon_extent(("Menlo", 60.0), None);
assert!(
small_editor_extent.abs_diff(large_editor_extent) <= 1,
"activity bar icon glyph extent must be editor-font-size independent: \
small_editor={small_editor_extent}, large_editor={large_editor_extent}"
);
let other_editor_family_extent = icon_extent(("Papyrus", 8.0), None);
assert!(
small_editor_extent.abs_diff(other_editor_family_extent) <= 1,
"activity bar icon glyph extent must be editor-font-family independent: \
menlo_editor={small_editor_extent}, papyrus_editor={other_editor_family_extent}"
);
let huge_ui_font_extent = icon_extent(("Menlo", 8.0), Some("Helvetica 60"));
assert!(
small_editor_extent.abs_diff(huge_ui_font_extent) <= 1,
"activity bar icon glyph extent must be ui-font-size independent (issue #1157): \
default_ui_font={small_editor_extent}, ui_font_Helvetica_60={huge_ui_font_extent}"
);
}
#[test]
#[allow(deprecated)] fn mac_backend_tab_bar_label_centres_on_chrome_line_height() {
const CW: u32 = 320;
const CH: u32 = 80;
const ROW_Y: f32 = 10.0;
const ROW_H: f32 = 60.0;
let surface = BitmapSurface::new(CW, CH);
surface.fill(0.0, 0.0, 0.0, 0.0);
let mut backend = MacBackend::new();
backend.set_current_font(super::super::text::make_font("Menlo", 5.0).expect("Menlo"));
backend.set_chrome_font(super::super::text::make_font("Menlo", 24.0).expect("Menlo"));
assert!(
backend.chrome_line_height - backend.current_line_height > 1.0,
"chrome and editor line heights must differ enough that the two centring \
formulas land at different y positions: chrome={}, editor={}",
backend.chrome_line_height,
backend.current_line_height,
);
let bar = TabBar {
id: WidgetId::new("tabbar:test"),
tabs: vec![crate::TabItem {
label: "a".to_string(),
is_active: true,
..Default::default()
}],
scroll_offset: 0,
right_segments: vec![],
active_accent: None,
show_tab_close: false,
compact: false,
};
backend.set_painted_text_recording(true);
backend.begin_frame(Viewport::new(CW as f32, CH as f32, 1.0));
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.draw_tab_bar(Rect::new(0.0, ROW_Y, CW as f32, ROW_H), &bar, None);
});
backend.end_frame();
let runs = backend.text_runs();
let run = runs.iter().find(|r| r.text == "a").unwrap_or_else(|| {
panic!(
"expected a painted run for the tab label \"a\"; painted: {:?}",
runs.iter().map(|r| r.text.as_str()).collect::<Vec<_>>()
)
});
let expected_y = ROW_Y + (ROW_H - backend.chrome_line_height as f32) / 2.0;
let buggy_y = ROW_Y + (ROW_H - backend.current_line_height as f32) / 2.0;
assert!(
(buggy_y - expected_y).abs() > 1.0,
"the two candidate centrings must differ enough to tell them apart: \
chrome-centred={expected_y}, editor-centred={buggy_y}",
);
assert!(
(run.bounds.y - expected_y).abs() < 0.01,
"tab label must be centred using chrome_line_height, not current_line_height: \
got y={}, expected={expected_y} (editor-metric centring would give {buggy_y})",
run.bounds.y,
);
}
}