#[cfg(not(target_os = "macos"))]
fn main() {}
#[cfg(target_os = "macos")]
fn main() {
native::run();
}
#[cfg(target_os = "macos")]
mod native {
use day_pieces::prelude::*;
use day_spec::{Size, WindowOptions};
use day_toolkit_appkit::AppKit;
use objc2::{
DefinedClass, MainThreadMarker, MainThreadOnly, Message, define_class, msg_send,
rc::Retained,
};
use objc2_app_kit::{
NSApplication, NSPasteboard, NSTableView, NSTableViewDropOperation, NSTextField, NSView,
};
use objc2_foundation::{NSIndexSet, NSObject, NSObjectProtocol, NSSize, NSString};
struct DragState {
table: Retained<NSTableView>,
board: Retained<NSPasteboard>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[ivars = DragState]
struct FixtureListDrag;
unsafe impl NSObjectProtocol for FixtureListDrag {}
impl FixtureListDrag {
#[unsafe(method_id(draggingSource))]
fn source(&self) -> Retained<NSTableView> { self.ivars().table.clone() }
#[unsafe(method_id(draggingPasteboard))]
fn board(&self) -> Retained<NSPasteboard> { self.ivars().board.clone() }
}
);
fn table_in(view: &NSView) -> Option<Retained<NSTableView>> {
if let Some(table) = view.downcast_ref::<NSTableView>() {
return Some(table.retain());
}
view.subviews().iter().find_map(|v| table_in(&v))
}
fn texts(view: &NSView) -> Vec<String> {
if let Some(label) = view.downcast_ref::<NSTextField>() {
return vec![label.stringValue().to_string()];
}
view.subviews().iter().flat_map(|v| texts(&v)).collect()
}
fn native_drop(table: &NSTableView, from: usize, insertion: usize) {
table.selectRowIndexes_byExtendingSelection(&NSIndexSet::indexSetWithIndex(from), false);
day_core::pump_events();
table.setDropRow_dropOperation(insertion as isize, NSTableViewDropOperation::Above);
let board = NSPasteboard::pasteboardWithUniqueName();
board.setString_forType(
&NSString::from_str(&from.to_string()),
&NSString::from_str("dev.daybrite.day.row"),
);
let drag = FixtureListDrag::alloc(MainThreadMarker::new().unwrap()).set_ivars(DragState {
table: table.retain(),
board,
});
let drag: Retained<FixtureListDrag> = unsafe { msg_send![super(drag), init] };
let delegate = unsafe { table.dataSource() }.unwrap();
let accepted: bool = unsafe {
msg_send![&*delegate, tableView: table, acceptDrop: &*drag, row: insertion as isize, dropOperation: NSTableViewDropOperation::Above]
};
assert!(accepted);
table.setDropRow_dropOperation(-1, NSTableViewDropOperation::Above);
}
fn check_scrolling(table: &NSTableView, rows: &[u64]) {
let scroll = table.enclosingScrollView().unwrap();
assert_eq!(table.numberOfRows(), rows.len() as isize);
assert!(
(table.frame().size.height - rows.len() as f64 * 44.).abs() < 1.,
"table height {} for {} rows",
table.frame().size.height,
rows.len()
);
for row in [rows.len() - 1, 0, rows.len() / 2, 0] {
table.scrollRowToVisible(row as isize);
table.layoutSubtreeIfNeeded();
let visible = table.rowsInRect(table.visibleRect());
assert!((visible.location..visible.location + visible.length).contains(&row));
let cell = table
.viewAtColumn_row_makeIfNecessary(0, row as isize, true)
.unwrap();
assert_eq!(texts(&cell), [format!("Fixture row {}", rows[row])]);
if row == rows.len() - 1 {
assert!(
scroll.contentView().bounds().origin.y > 2000.,
"can reach the last row after a drop"
);
} else if row == 0 {
assert!(
scroll.contentView().bounds().origin.y < 1.,
"can return to the first row"
);
}
}
}
pub fn run() {
std::thread::spawn(|| {
std::thread::sleep(std::time::Duration::from_secs(20));
eprintln!("native list scrolling test timed out");
std::process::exit(2);
});
day_core::launch_with(
AppKit::new(),
WindowOptions {
title: "Fixture reorder scrolling".into(),
size: Size::new(500., 500.),
..Default::default()
},
|| {
let rows = Signal::new((0..60u64).collect::<Vec<_>>());
let selected = Signal::new(None::<u64>);
day_core::task(async move {
day_core::sleep(200).await;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let app =
NSApplication::sharedApplication(MainThreadMarker::new().unwrap());
let table =
table_in(&app.windows().objectAtIndex(0).contentView().unwrap())
.unwrap();
check_scrolling(&table, &rows.get());
native_drop(&table, 0, 4);
table.setFrameSize(NSSize::new(table.frame().size.width, 280.));
table
}));
let table = match result {
Ok(table) => table,
Err(_) => std::process::exit(1),
};
day_core::sleep(200).await;
for row in (0..60).step_by(5) {
table.scrollRowToVisible(row);
table.layoutSubtreeIfNeeded();
day_core::sleep(10).await;
}
let active = day_core::with_tree(|t| {
let mut node = t.find_by_id("fixture_list").unwrap();
while t.node_kind(node) != Some(day_spec::kinds::LIST) {
node = t.first_child(node).unwrap();
}
t.list_cell_keys(node).len()
});
assert!(
(1..30).contains(&active),
"retired cells still participate in layout: {active}"
);
println!("native list active layout cells after scrolling: {active}");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_eq!(&rows.get()[..4], &[1, 2, 3, 0]);
assert_eq!(selected.get(), Some(0));
assert_eq!(table.selectedRow(), 3);
check_scrolling(&table, &rows.get());
table.scrollRowToVisible(30);
let origin = table
.enclosingScrollView()
.unwrap()
.contentView()
.bounds()
.origin;
native_drop(&table, 30, 33);
origin
}));
let origin = match result {
Ok(origin) => origin,
Err(_) => std::process::exit(1),
};
day_core::sleep(200).await;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_eq!(
table
.enclosingScrollView()
.unwrap()
.contentView()
.bounds()
.origin,
origin
);
assert_eq!(selected.get(), Some(30));
assert_eq!(table.selectedRow(), 32);
check_scrolling(&table, &rows.get());
native_drop(&table, 0, 2);
day_core::initial_window().unwrap().close();
}));
if result.is_err() {
std::process::exit(1);
}
day_core::sleep(200).await;
println!(
"native list drop: scroll extent recovery, row content, selection and viewport preservation passed"
);
std::process::exit(0);
});
scroll(
column((
label("Synthetic list fixture"),
list(items(move || rows.get(), |r: &u64| *r), move |slot| {
label(move || format!("Fixture row {}", slot.get()))
.padding(10.)
.grow_w()
})
.on_selection(move |ids| selected.set(ids.first().copied()))
.selected_rows(move || {
rows.with(|r| {
r.iter()
.position(|v| Some(*v) == selected.get())
.into_iter()
.collect()
})
})
.reorderable(Reorder::new(move |from, to| {
rows.update(|r| {
let v = r.remove(from);
r.insert(to, v);
})
}))
.row_height(RowHeight::Uniform(44.))
.height(280.)
.grow_w()
.id(day_core::id!(fixture_list)),
label(move || format!("{} fixture rows", rows.with(|r| r.len()))),
))
.spacing(10.)
.padding(20.),
)
.grow()
.any()
},
);
}
}