use std::collections::HashMap;
use std::time::{Duration, Instant};
use day_spec::ffi_guard;
use day_spec::sidetable::SideTable;
use day_spec::{
Event, Icon, NodeId, Symbol, ToolbarItem, ToolbarItemKind, ToolbarPatch, ToolbarValue,
};
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, NSObjectProtocol, ProtocolObject};
use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel};
use objc2_app_kit::{
NSBezelStyle, NSButton, NSControl, NSControlStateValueOff, NSControlStateValueOn, NSImage,
NSMenuToolbarItem, NSSearchToolbarItem, NSTextField, NSToolbar, NSToolbarDelegate,
NSToolbarDisplayMode, NSToolbarFlexibleSpaceItemIdentifier, NSToolbarItem,
NSToolbarItemIdentifier, NSToolbarSpaceItemIdentifier, NSView, NSWindow, NSWindowToolbarStyle,
};
use objc2_foundation::{NSArray, NSCopying, NSObject, NSString};
use crate::{AppKit, Handle, emit};
fn refresh_cover_bar(key: usize) {
if let Some(bar) = BARS.with(|b| b.with(key, |w| w.toolbar.clone())) {
reconcile(&bar, key);
}
}
pub(crate) fn cover_presented(cover: usize, window: &NSWindow) {
if COVER_WINDOWS.with(|c| c.contains(cover)) {
return;
}
let key = window as *const NSWindow as usize;
COVER_COUNTS.with(|c| {
let count = c.with(key, |n| *n).unwrap_or(0);
c.insert(key, count + 1);
});
COVER_WINDOWS.with(|c| c.insert(cover, key));
refresh_cover_bar(key);
}
pub(crate) fn cover_dismissed(cover: usize) {
COVER_WINDOWS.with(|c| {
c.remove(cover);
});
}
fn sf_symbol(s: Symbol) -> &'static str {
day_spec::sf_symbol_name(s)
}
pub fn image_for(icon: &Icon, label: &str, mtm: MainThreadMarker) -> Option<Retained<NSImage>> {
match icon {
Icon::Symbol(s) => {
let name = sf_symbol(*s);
if name.is_empty() {
return None;
}
NSImage::imageWithSystemSymbolName_accessibilityDescription(
&NSString::from_str(name),
Some(&NSString::from_str(label)),
)
}
Icon::Image(name) => {
let _ = mtm;
let path = day_spec::resource::resolve_vector_svg(name)
.or_else(|| day_spec::resource::resolve_image_file(name))?;
use objc2::AllocAnyThread as _;
let img = unsafe {
NSImage::initWithContentsOfFile(
NSImage::alloc(),
&NSString::from_str(&path.to_string_lossy()),
)
}?;
unsafe { img.setTemplate(true) };
Some(img)
}
}
}
const KIND_BUTTON: u8 = 0;
const KIND_TOGGLE: u8 = 1;
const KIND_SEARCH: u8 = 2;
const KIND_SEGMENTED: u8 = 3;
struct ItemIvars {
action: u64,
kind: u8,
text: std::cell::RefCell<String>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "DayToolbarTarget"]
#[ivars = ItemIvars]
struct ItemTarget;
unsafe impl NSObjectProtocol for ItemTarget {}
impl ItemTarget {
#[unsafe(method(fire:))]
fn fire(&self, sender: &AnyObject) {
ffi_guard::contain((), || {
let ivars = self.ivars();
match ivars.kind {
KIND_SEARCH => {
if let Some(field) = sender.downcast_ref::<NSTextField>() {
let text = field.stringValue().to_string();
if *ivars.text.borrow() != text {
self.send_text(text);
}
}
}
KIND_TOGGLE => {
let on = sender
.downcast_ref::<NSButton>()
.map(|b| b.state() == NSControlStateValueOn)
.unwrap_or(false);
emit(
day_spec::WINDOW_NODE,
Event::ToolbarChanged {
action: ivars.action,
value: ToolbarValue::On(on),
},
);
}
KIND_SEGMENTED => {
let index = sender
.downcast_ref::<objc2_app_kit::NSSegmentedControl>()
.map(|c| unsafe { c.selectedSegment() })
.unwrap_or(0);
if index >= 0 {
emit(
day_spec::WINDOW_NODE,
Event::ToolbarChanged {
action: ivars.action,
value: ToolbarValue::Selected(index as usize),
},
);
}
}
_ => emit(day_spec::WINDOW_NODE, Event::MenuAction(ivars.action)),
}
})
}
}
);
impl ItemTarget {
fn new(mtm: MainThreadMarker, action: u64, kind: u8, text: &str) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(ItemIvars {
action,
kind,
text: std::cell::RefCell::new(text.to_owned()),
});
unsafe { msg_send![super(this), init] }
}
fn send_text(&self, text: String) {
self.ivars().text.replace(text.clone());
emit(
day_spec::WINDOW_NODE,
Event::ToolbarChanged {
action: self.ivars().action,
value: ToolbarValue::Text(text),
},
);
}
fn submit(&self, text: String) {
if *self.ivars().text.borrow() != text {
self.send_text(text.clone());
}
emit(
day_spec::WINDOW_NODE,
Event::ToolbarChanged {
action: self.ivars().action,
value: ToolbarValue::Submit(text),
},
);
}
}
define_class!(
#[unsafe(super(objc2_app_kit::NSSearchField))]
#[thread_kind = MainThreadOnly]
#[name = "DayToolbarSearchField"]
struct SearchField;
impl SearchField {
#[unsafe(method(textView:doCommandBySelector:))]
fn do_command(
&self,
tv: &objc2_app_kit::NSTextView,
command: objc2::runtime::Sel,
) -> objc2::runtime::Bool {
ffi_guard::contain(objc2::runtime::Bool::NO, || {
if command == sel!(insertNewline:)
&& let Some(target) = unsafe { self.target() }
&& let Some(target) = target.downcast_ref::<ItemTarget>()
{
target.submit(self.stringValue().to_string());
return objc2::runtime::Bool::YES;
}
unsafe { msg_send![super(self), textView: tv, doCommandBySelector: command] }
})
}
}
);
impl SearchField {
fn new(mtm: MainThreadMarker) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
struct BarIvars {
key: usize,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "DayToolbarDelegate"]
#[ivars = BarIvars]
struct BarDelegate;
unsafe impl NSObjectProtocol for BarDelegate {}
unsafe impl NSToolbarDelegate for BarDelegate {
#[unsafe(method_id(toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:))]
fn item_for_identifier(
&self,
_toolbar: &NSToolbar,
identifier: &NSToolbarItemIdentifier,
_inserted: bool,
) -> Option<Retained<NSToolbarItem>> {
ffi_guard::contain(None, || {
let mtm = MainThreadMarker::from(self);
make_item(mtm, self.ivars().key, &identifier.to_string())
})
}
#[unsafe(method_id(toolbarDefaultItemIdentifiers:))]
fn default_identifiers(
&self,
_toolbar: &NSToolbar,
) -> Retained<NSArray<NSToolbarItemIdentifier>> {
identifiers(self.ivars().key)
}
#[unsafe(method_id(toolbarAllowedItemIdentifiers:))]
fn allowed_identifiers(
&self,
_toolbar: &NSToolbar,
) -> Retained<NSArray<NSToolbarItemIdentifier>> {
identifiers(self.ivars().key)
}
}
);
impl BarDelegate {
fn new(mtm: MainThreadMarker, key: usize) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(BarIvars { key });
unsafe { msg_send![super(this), init] }
}
}
struct WinToolbar {
toolbar: Retained<NSToolbar>,
_delegate: Retained<BarDelegate>,
mirror: day_spec::ToolbarMirror,
targets: HashMap<String, Retained<ItemTarget>>,
}
const CHROME_SETTLE: Duration = Duration::from_millis(120);
thread_local! {
static CHROME_SETTLES_AT: std::cell::Cell<Option<Instant>> = const { std::cell::Cell::new(None) };
}
pub(crate) fn chrome_settled() -> bool {
CHROME_SETTLES_AT.with(|at| match at.get() {
Some(until) if Instant::now() < until => false,
Some(_) => {
at.set(None);
true
}
None => true,
})
}
day_core::tls_group! {
static BARS: SideTable<WinToolbar> = SideTable::with_teardown(|w: WinToolbar| {
w.toolbar.setDelegate(None);
});
static COVER_COUNTS: SideTable<usize> = SideTable::new();
static COVER_WINDOWS: SideTable<usize> = SideTable::with_teardown(|key| {
COVER_COUNTS.with(|c| { c.with(key, |n| *n = n.saturating_sub(1)); });
refresh_cover_bar(key);
});
static NEXT_BAR: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
}
fn identifiers(key: usize) -> Retained<NSArray<NSToolbarItemIdentifier>> {
use day_spec::ToolbarColumn as C;
let names: Vec<Retained<NSString>> = BARS.with(|b| {
b.with(key, |w| {
if COVER_COUNTS.with(|c| c.with(key, |n| *n > 0).unwrap_or(false)) {
return Vec::new();
}
let mut out: Vec<Retained<NSString>> = Vec::new();
let items = w.mirror.items();
let has = |c: C| items.iter().any(|i| i.column == c);
if has(C::Sidebar) {
out.push(unsafe { NSToolbarFlexibleSpaceItemIdentifier.copy() });
column_items(&mut out, items, C::Sidebar, false);
}
out.push(unsafe {
objc2_app_kit::NSToolbarSidebarTrackingSeparatorItemIdentifier.copy()
});
if has(C::List) {
column_items(&mut out, items, C::List, true);
out.push(NSString::from_str(LIST_SEPARATOR_ID));
}
column_items(&mut out, items, C::Detail, true);
column_items(&mut out, items, C::Window, true);
out
})
.unwrap_or_default()
});
let refs: Vec<&NSToolbarItemIdentifier> = names.iter().map(|n| n.as_ref()).collect();
NSArray::from_slice(&refs)
}
fn column_items(
out: &mut Vec<Retained<NSString>>,
items: &[ToolbarItem],
col: day_spec::ToolbarColumn,
spread: bool,
) {
use day_spec::ToolbarPlacement as P;
let mine: Vec<&ToolbarItem> = items.iter().filter(|i| i.column == col).collect();
if mine.is_empty() {
return;
}
let lead = [P::Navigation, P::Automatic];
let trail = [P::Primary, P::Secondary, P::Bottom];
let has_lead = mine.iter().any(|i| lead.contains(&i.placement));
let has_trail = mine.iter().any(|i| trail.contains(&i.placement));
for i in mine.iter().filter(|i| lead.contains(&i.placement)) {
out.push(identifier_of(i));
}
if spread && has_lead && has_trail {
out.push(unsafe { NSToolbarFlexibleSpaceItemIdentifier.copy() });
}
for i in mine.iter().filter(|i| i.placement == P::Principal) {
out.push(identifier_of(i));
}
if spread && !has_lead && has_trail {
out.push(unsafe { NSToolbarFlexibleSpaceItemIdentifier.copy() });
}
for i in mine.iter().filter(|i| trail.contains(&i.placement)) {
out.push(identifier_of(i));
}
}
const LIST_SEPARATOR_ID: &str = "day.toolbar.list-separator";
fn identifier_of(item: &ToolbarItem) -> Retained<NSString> {
if item.id == day_spec::SIDEBAR_TOGGLE_ID {
return unsafe { objc2_app_kit::NSToolbarToggleSidebarItemIdentifier.copy() };
}
match item.kind {
ToolbarItemKind::Separator => unsafe { NSToolbarSpaceItemIdentifier.copy() },
_ => NSString::from_str(&item.id),
}
}
fn make_item(mtm: MainThreadMarker, key: usize, ident: &str) -> Option<Retained<NSToolbarItem>> {
if ident == LIST_SEPARATOR_ID {
let _ = mtm;
let split = crate::list_split_view(key)?;
return Some(Retained::into_super(unsafe {
objc2_app_kit::NSTrackingSeparatorToolbarItem::
trackingSeparatorToolbarItemWithIdentifier_splitView_dividerIndex(
&NSString::from_str(LIST_SEPARATOR_ID),
&split,
1,
)
}));
}
let (item, target) = BARS.with(|b| {
b.with(key, |w| {
let item = w.mirror.get(ident)?.clone();
let target = w.targets.get(ident).cloned();
Some((item, target))
})
.flatten()
})?;
let id = NSString::from_str(&item.id);
let label = NSString::from_str(&item.label);
let tip = NSString::from_str(item.tooltip.as_deref().unwrap_or(&item.label));
let bar_item: Retained<NSToolbarItem> = match &item.kind {
ToolbarItemKind::Search {
text, placeholder, ..
} => {
let search =
NSSearchToolbarItem::initWithItemIdentifier(NSSearchToolbarItem::alloc(mtm), &id);
let field = SearchField::new(mtm);
search.setSearchField(&field);
field.setStringValue(&NSString::from_str(text));
if !placeholder.is_empty() {
field.setPlaceholderString(Some(&NSString::from_str(placeholder)));
}
if let Some(t) = &target {
field.setSendsSearchStringImmediately(true);
field.setSendsWholeSearchString(false);
unsafe {
field.setTarget(Some(&**t));
field.setAction(Some(sel!(fire:)));
}
}
Retained::into_super(search)
}
ToolbarItemKind::Menu { items } => {
let menu_item =
NSMenuToolbarItem::initWithItemIdentifier(NSMenuToolbarItem::alloc(mtm), &id);
let menu = crate::build_ns_menu(mtm, &item.label, items);
menu_item.setMenu(&menu);
if let Some(icon) = &item.icon
&& let Some(img) = image_for(icon, &item.label, mtm)
{
menu_item.setImage(Some(&img));
}
Retained::into_super(menu_item)
}
ToolbarItemKind::Toggle { on } => {
let bar_item = NSToolbarItem::initWithItemIdentifier(NSToolbarItem::alloc(mtm), &id);
let button = unsafe {
NSButton::buttonWithTitle_target_action(
&label,
target.as_deref().map(|t| t as &AnyObject),
Some(sel!(fire:)),
mtm,
)
};
button.setBezelStyle(NSBezelStyle::Toolbar);
unsafe { button.setButtonType(objc2_app_kit::NSButtonType::PushOnPushOff) };
if let Some(icon) = &item.icon
&& let Some(img) = image_for(icon, &item.label, mtm)
{
button.setImage(Some(&img));
button.setTitle(&NSString::from_str(""));
}
button.setState(if *on {
NSControlStateValueOn
} else {
NSControlStateValueOff
});
bar_item.setView(Some(button.as_ref() as &NSView));
bar_item
}
ToolbarItemKind::Segmented { segments, selected } => {
let bar_item = NSToolbarItem::initWithItemIdentifier(NSToolbarItem::alloc(mtm), &id);
let control = unsafe {
objc2_app_kit::NSSegmentedControl::initWithFrame(
objc2_app_kit::NSSegmentedControl::alloc(mtm),
objc2_foundation::NSRect::new(
objc2_foundation::NSPoint::new(0.0, 0.0),
objc2_foundation::NSSize::new((segments.len() as f64) * 44.0, 24.0),
),
)
};
unsafe {
control.setSegmentCount(segments.len() as isize);
control.setSegmentStyle(objc2_app_kit::NSSegmentStyle::Automatic);
control.setTrackingMode(objc2_app_kit::NSSegmentSwitchTracking::SelectOne);
for (i, seg) in segments.iter().enumerate() {
let i = i as isize;
match seg
.icon
.as_ref()
.and_then(|ic| image_for(ic, &seg.title, mtm))
{
Some(img) => {
control.setImage_forSegment(Some(&img), i);
control.setLabel_forSegment(&NSString::from_str(""), i);
}
None => control.setLabel_forSegment(&NSString::from_str(&seg.title), i),
}
let _: () = msg_send![
&*control,
setToolTip: &*NSString::from_str(&seg.title),
forSegment: i,
];
}
if *selected < segments.len() {
control.setSelectedSegment(*selected as isize);
}
if let Some(t) = target.as_deref() {
control.setTarget(Some(t as &AnyObject));
control.setAction(Some(sel!(fire:)));
}
}
bar_item.setView(Some(control.as_ref() as &NSView));
bar_item
}
ToolbarItemKind::Label => {
let bar_item = NSToolbarItem::initWithItemIdentifier(NSToolbarItem::alloc(mtm), &id);
let field = NSTextField::labelWithString(&label, mtm);
bar_item.setView(Some(field.as_ref() as &NSView));
bar_item
}
_ => {
let bar_item = NSToolbarItem::initWithItemIdentifier(NSToolbarItem::alloc(mtm), &id);
if let Some(icon) = &item.icon
&& let Some(img) = image_for(icon, &item.label, mtm)
{
bar_item.setImage(Some(&img));
}
if bar_item.image().is_none() {
let button = unsafe {
NSButton::buttonWithTitle_target_action(
&label,
target.as_deref().map(|t| t as &AnyObject),
Some(sel!(fire:)),
mtm,
)
};
button.setBezelStyle(NSBezelStyle::Automatic);
unsafe { button.setEnabled(item.enabled) };
bar_item.setView(Some(button.as_ref() as &NSView));
}
bar_item.setBordered(true);
if let Some(t) = &target {
unsafe {
bar_item.setTarget(Some(&**t as &AnyObject));
bar_item.setAction(Some(sel!(fire:)));
}
}
bar_item
}
};
bar_item.setLabel(&label);
bar_item.setPaletteLabel(&label);
bar_item.setToolTip(Some(&tip));
bar_item.setAutovalidates(false);
bar_item.setEnabled(item.enabled);
Some(bar_item)
}
pub(crate) fn window_of(h: &Handle) -> Option<Retained<NSWindow>> {
h.window()
}
impl AppKit {
pub(crate) fn edit_toolbar(&mut self, h: &Handle, ops: &[day_spec::ToolbarOp]) -> bool {
let Some(window) = window_of(h) else {
return false;
};
let key = Retained::as_ptr(&window) as usize;
let mtm = self.mtm();
let fresh = !BARS.with(|b| b.contains(key));
if fresh {
let ident = NEXT_BAR.with(|c| {
let n = c.get();
c.set(n + 1);
n
});
let toolbar = NSToolbar::initWithIdentifier(
NSToolbar::alloc(mtm),
&NSString::from_str(&format!("day.toolbar.{ident}")),
);
let delegate = BarDelegate::new(mtm, key);
toolbar.setAllowsUserCustomization(false);
toolbar.setAutosavesConfiguration(false);
toolbar.setDisplayMode(NSToolbarDisplayMode::IconOnly);
BARS.with(|b| {
b.insert(
key,
WinToolbar {
toolbar,
_delegate: delegate,
mirror: day_spec::ToolbarMirror::default(),
targets: HashMap::new(),
},
)
});
}
let mut retired: Vec<Retained<ItemTarget>> = Vec::new();
let mut gone: Vec<String> = Vec::new();
let (toolbar, empty) = BARS
.with(|b| {
b.with(key, |w| {
for op in ops {
match op {
day_spec::ToolbarOp::Remove { id } => {
w.mirror.remove(id);
retired.extend(w.targets.remove(id));
gone.push(id.clone());
}
day_spec::ToolbarOp::Insert { index, item } => {
if item.action != 0 {
let kind = match item.kind {
ToolbarItemKind::Toggle { .. } => KIND_TOGGLE,
ToolbarItemKind::Segmented { .. } => KIND_SEGMENTED,
ToolbarItemKind::Search { .. } => KIND_SEARCH,
_ => KIND_BUTTON,
};
let text = match &item.kind {
ToolbarItemKind::Search { text, .. } => text.as_str(),
_ => "",
};
let target = ItemTarget::new(mtm, item.action, kind, text);
retired.extend(w.targets.insert(item.id.clone(), target));
}
w.mirror.insert(*index, item.clone());
}
}
}
(w.toolbar.clone(), w.mirror.is_empty())
})
})
.expect("the bar was created above");
if empty {
window.setToolbar(None);
BARS.with(|b| {
b.remove(key);
});
} else if fresh {
let delegate = BARS.with(|b| b.with(key, |w| w._delegate.clone()));
if let Some(delegate) = delegate {
toolbar.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
}
window.setToolbarStyle(NSWindowToolbarStyle::Unified);
window.setToolbar(Some(&toolbar));
} else {
let at_of = |id: &str| {
toolbar
.items()
.iter()
.position(|i| i.itemIdentifier().to_string() == id)
};
for id in &gone {
if let Some(at) = at_of(id) {
toolbar.removeItemAtIndex(at as isize);
}
}
reconcile(&toolbar, key);
}
drop(retired);
report_content_size(&window);
CHROME_SETTLES_AT.with(|at| at.set(Some(Instant::now() + CHROME_SETTLE)));
true
}
pub(crate) fn patch_toolbar(&mut self, h: &Handle, patch: &ToolbarPatch) {
let Some(window) = window_of(h) else { return };
let key = Retained::as_ptr(&window) as usize;
BARS.with(|b| {
b.with(key, |w| {
w.mirror.patch(patch);
if let ToolbarPatch::Text { item, text } = patch
&& let Some(target) = w.targets.get(item)
{
target.ivars().text.replace(text.clone());
}
});
});
let Some(toolbar) = BARS.with(|b| b.with(key, |w| w.toolbar.clone())) else {
return;
};
let target_id = patch.item().to_string();
for bar_item in toolbar.items().iter() {
if bar_item.itemIdentifier().to_string() != target_id {
continue;
}
match patch {
ToolbarPatch::Focus { .. } => {
if let Some(search) = bar_item.downcast_ref::<NSSearchToolbarItem>() {
search.beginSearchInteraction();
unsafe {
search.searchField().selectText(None);
}
}
}
ToolbarPatch::Text { text, .. } => {
if let Some(search) = bar_item.downcast_ref::<NSSearchToolbarItem>() {
let field = search.searchField();
if field.stringValue().to_string() != *text {
field.setStringValue(&NSString::from_str(text));
}
}
}
ToolbarPatch::On { on, .. } => {
if let Some(view) = bar_item.view()
&& let Some(button) = view.downcast_ref::<NSButton>()
{
button.setState(if *on {
NSControlStateValueOn
} else {
NSControlStateValueOff
});
}
}
ToolbarPatch::Selected { index, .. } => {
if let Some(view) = bar_item.view()
&& let Some(seg) = view.downcast_ref::<objc2_app_kit::NSSegmentedControl>()
{
unsafe { seg.setSelectedSegment(*index as isize) };
}
}
ToolbarPatch::Enabled { on, .. } => {
bar_item.setEnabled(*on);
if let Some(view) = bar_item.view()
&& let Some(control) = view.downcast_ref::<NSControl>()
{
unsafe {
control.setEnabled(*on);
}
}
}
ToolbarPatch::Suggestions { .. } => {}
ToolbarPatch::Label { label, tooltip, .. } => {
let words = NSString::from_str(label);
bar_item.setLabel(&words);
bar_item.setPaletteLabel(&words);
let tip = NSString::from_str(tooltip.as_deref().unwrap_or(label));
bar_item.setToolTip(Some(&tip));
}
ToolbarPatch::Placeholder { text, .. } => {
if let Some(search) = bar_item.downcast_ref::<NSSearchToolbarItem>() {
search
.searchField()
.setPlaceholderString(Some(&NSString::from_str(text)));
}
}
}
}
}
}
fn reconcile(toolbar: &NSToolbar, key: usize) {
let want: Vec<String> = identifiers(key).iter().map(|i| i.to_string()).collect();
let system = |id: &str| id.starts_with("NSToolbar") || id == LIST_SEPARATOR_ID;
let have_at = |at: usize| {
let items = toolbar.items();
(at < items.count()).then(|| items.objectAtIndex(at).itemIdentifier().to_string())
};
let mut at = 0usize;
for (n, id) in want.iter().enumerate() {
loop {
match have_at(at) {
Some(have) if have == *id => {
at += 1;
break;
}
Some(have) if !system(&have) && want[n + 1..].contains(&have) => {
insert_at(toolbar, id, &mut at);
break;
}
Some(_) => toolbar.removeItemAtIndex(at as isize),
None => {
insert_at(toolbar, id, &mut at);
break;
}
}
}
}
while toolbar.items().count() > at {
toolbar.removeItemAtIndex(at as isize);
}
}
fn insert_at(toolbar: &NSToolbar, id: &str, at: &mut usize) {
let repeatable = id == unsafe { NSToolbarFlexibleSpaceItemIdentifier.to_string() }
|| id == unsafe { NSToolbarSpaceItemIdentifier.to_string() };
if !repeatable
&& let Some(existing) = toolbar
.items()
.iter()
.position(|item| item.itemIdentifier().to_string() == id)
{
toolbar.removeItemAtIndex(existing as isize);
if existing < *at {
*at -= 1;
}
}
let before = toolbar.items().count();
toolbar.insertItemWithItemIdentifier_atIndex(&NSString::from_str(id), *at as isize);
if toolbar.items().count() > before {
*at += 1;
}
}
fn report_content_size(window: &NSWindow) {
let Some(size) = crate::pin_below_title_bar(window) else {
return;
};
let node: NodeId = window
.delegate()
.and_then(|d| d.downcast::<crate::DayWinDelegate>().ok())
.and_then(|d| d.ivars().node)
.unwrap_or(day_spec::WINDOW_NODE);
emit(node, Event::WindowResized(size));
}