diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 521d550..0805a91 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -359,3 +359,31 @@ path = "examples/native_switch.rs" [[example]] name = "native_toggle_group" path = "examples/native_toggle_group.rs" + +[[example]] +name = "native_collection_view" +path = "examples/native_collection_view.rs" + +[[example]] +name = "native_table_view" +path = "examples/native_table_view.rs" + +[[example]] +name = "native_outline_view" +path = "examples/native_outline_view.rs" + +[[example]] +name = "native_menu_button" +path = "examples/native_menu_button.rs" + +[[example]] +name = "native_tab_view" +path = "examples/native_tab_view.rs" + +[[example]] +name = "native_table_styles" +path = "examples/native_table_styles.rs" + +[[example]] +name = "native_list_views" +path = "examples/native_list_views.rs" diff --git a/crates/gpui/examples/native_collection_view.rs b/crates/gpui/examples/native_collection_view.rs new file mode 100644 index 0000000..210ff34 --- /dev/null +++ b/crates/gpui/examples/native_collection_view.rs @@ -0,0 +1,84 @@ +use gpui::{ + App, Application, Bounds, CollectionSelectEvent, Context, NativeCollectionItemStyle, Window, + WindowAppearance, WindowBounds, WindowOptions, div, native_collection_view, prelude::*, px, + rgb, size, +}; + +struct CollectionViewExample { + selected: Option, +} + +impl CollectionViewExample { + const APPS: [&str; 12] = [ + "Calendar", + "Mail", + "Notes", + "Music", + "Maps", + "Photos", + "Books", + "TV", + "Stocks", + "Weather", + "Shortcuts", + "Xcode", + ]; +} + +impl Render for CollectionViewExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_dark = matches!( + window.appearance(), + WindowAppearance::Dark | WindowAppearance::VibrantDark + ); + let (bg, fg, muted) = if is_dark { + (rgb(0x1b1d22), rgb(0xffffff), rgb(0xb5bcc8)) + } else { + (rgb(0xf4f6fa), rgb(0x1a2230), rgb(0x5d6676)) + }; + + div() + .flex() + .flex_col() + .size_full() + .gap_3() + .p_4() + .bg(bg) + .text_color(fg) + .child(div().text_xl().child("Native CollectionView (Cards)")) + .child( + native_collection_view("apps", &Self::APPS) + .columns(3) + .item_height(72.0) + .item_style(NativeCollectionItemStyle::Card) + .selected_index(self.selected) + .on_select(cx.listener(|this, event: &CollectionSelectEvent, _, cx| { + this.selected = Some(event.index); + cx.notify(); + })) + .h(px(300.0)), + ) + .child(div().text_sm().text_color(muted).child(format!( + "Selected: {}", + self.selected + .map(|i| Self::APPS[i].to_string()) + .unwrap_or_else(|| "".to_string()) + ))) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(760.), px(520.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| CollectionViewExample { selected: None }), + ) + .unwrap(); + + cx.activate(true); + }); +} diff --git a/crates/gpui/examples/native_list_views.rs b/crates/gpui/examples/native_list_views.rs new file mode 100644 index 0000000..018b172 --- /dev/null +++ b/crates/gpui/examples/native_list_views.rs @@ -0,0 +1,182 @@ +use gpui::{ + App, Application, Bounds, CollectionSelectEvent, Context, NativeCollectionItemStyle, + NativeOutlineNode, OutlineRowSelectEvent, TableRowSelectEvent, Window, WindowAppearance, + WindowBounds, WindowOptions, div, native_collection_view, native_outline_view, + native_table_view, prelude::*, px, rgb, size, +}; + +struct ListViewsExample { + selected_collection: Option, + selected_table: Option, + selected_outline: String, +} + +impl ListViewsExample { + const ITEMS: [&str; 10] = [ + "Accounts", + "Projects", + "Deployments", + "Domains", + "Settings", + "Members", + "Tokens", + "Alerts", + "Logs", + "Billing", + ]; + + fn outline_nodes() -> Vec { + vec![ + NativeOutlineNode::branch( + "Workspace", + vec![ + NativeOutlineNode::leaf("Overview"), + NativeOutlineNode::leaf("Members"), + NativeOutlineNode::leaf("Audit Log"), + ], + ), + NativeOutlineNode::branch( + "Applications", + vec![ + NativeOutlineNode::leaf("Glass"), + NativeOutlineNode::leaf("Console"), + NativeOutlineNode::leaf("Dashboard"), + ], + ), + ] + } +} + +impl Render for ListViewsExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_dark = matches!( + window.appearance(), + WindowAppearance::Dark | WindowAppearance::VibrantDark + ); + let (bg, fg, muted) = if is_dark { + (rgb(0x171b22), rgb(0xffffff), rgb(0xb7c0ce)) + } else { + (rgb(0xf5f7fb), rgb(0x1b2434), rgb(0x606a7c)) + }; + + div() + .flex() + .flex_col() + .size_full() + .gap_3() + .p_4() + .bg(bg) + .text_color(fg) + .child( + div() + .text_xl() + .child("Native Lists: Collection / Table / Outline"), + ) + .child( + div() + .flex() + .gap_3() + .child( + div() + .flex() + .flex_col() + .gap_2() + .child(div().text_sm().child("NSCollectionView (list mode)")) + .child( + native_collection_view("collection", &Self::ITEMS) + .columns(1) + .item_height(34.0) + .spacing(2.0) + .item_style(NativeCollectionItemStyle::Label) + .selected_index(self.selected_collection) + .on_select(cx.listener( + |this, event: &CollectionSelectEvent, _, cx| { + this.selected_collection = Some(event.index); + cx.notify(); + }, + )) + .w(px(260.0)) + .h(px(360.0)), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .child(div().text_sm().child("NSTableView (source list style)")) + .child( + native_table_view("table", &Self::ITEMS) + .table_style(gpui::NativeTableStyle::SourceList) + .row_size_style(gpui::NativeTableRowSizeStyle::Small) + .show_header(false) + .alternating_rows(false) + .selected_index(self.selected_table) + .on_select(cx.listener( + |this, event: &TableRowSelectEvent, _, cx| { + this.selected_table = Some(event.index); + cx.notify(); + }, + )) + .w(px(260.0)) + .h(px(360.0)), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .child(div().text_sm().child("NSOutlineView")) + .child( + native_outline_view("outline", &Self::outline_nodes()) + .expand_all(true) + .on_select(cx.listener( + |this, event: &OutlineRowSelectEvent, _, cx| { + this.selected_outline = event.title.to_string(); + cx.notify(); + }, + )) + .w(px(260.0)) + .h(px(360.0)), + ), + ), + ) + .child(div().text_sm().text_color(muted).child(format!( + "Collection: {} | Table: {} | Outline: {}", + self.selected_collection + .map(|idx| Self::ITEMS[idx].to_string()) + .unwrap_or_else(|| "".to_string()), + self.selected_table + .map(|idx| Self::ITEMS[idx].to_string()) + .unwrap_or_else(|| "".to_string()), + if self.selected_outline.is_empty() { + "".to_string() + } else { + self.selected_outline.clone() + } + ))) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(920.), px(540.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| { + cx.new(|_| ListViewsExample { + selected_collection: None, + selected_table: None, + selected_outline: String::new(), + }) + }, + ) + .unwrap(); + + cx.activate(true); + }); +} diff --git a/crates/gpui/examples/native_menu_button.rs b/crates/gpui/examples/native_menu_button.rs new file mode 100644 index 0000000..22f1252 --- /dev/null +++ b/crates/gpui/examples/native_menu_button.rs @@ -0,0 +1,98 @@ +use gpui::{ + App, Application, Bounds, Context, MenuItemSelectEvent, NativeMenuItem, Window, + WindowAppearance, WindowBounds, WindowOptions, div, native_context_menu, native_menu_button, + prelude::*, px, rgb, size, +}; + +struct MenuExample { + selected_index: Option, +} + +impl MenuExample { + fn menu() -> Vec { + vec![ + NativeMenuItem::action("Open"), + NativeMenuItem::action("Duplicate"), + NativeMenuItem::separator(), + NativeMenuItem::submenu( + "Export", + vec![ + NativeMenuItem::action("PNG"), + NativeMenuItem::action("PDF"), + NativeMenuItem::action("SVG"), + ], + ), + NativeMenuItem::separator(), + NativeMenuItem::action("Delete").enabled(false), + ] + } +} + +impl Render for MenuExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_dark = matches!( + window.appearance(), + WindowAppearance::Dark | WindowAppearance::VibrantDark + ); + let (bg, fg, muted) = if is_dark { + (rgb(0x171b21), rgb(0xffffff), rgb(0xb1bbc8)) + } else { + (rgb(0xf4f7fb), rgb(0x1a2434), rgb(0x5f6a7b)) + }; + + let menu = Self::menu(); + + div() + .flex() + .flex_col() + .size_full() + .items_center() + .justify_center() + .gap_4() + .bg(bg) + .text_color(fg) + .child(div().text_xl().child("Native MenuButton + ContextMenu")) + .child( + native_menu_button("actions", "Actions", &menu).on_select(cx.listener( + |this, event: &MenuItemSelectEvent, _, cx| { + this.selected_index = Some(event.index); + cx.notify(); + }, + )), + ) + .child( + native_context_menu("context", "Right click me", &menu).on_select(cx.listener( + |this, event: &MenuItemSelectEvent, _, cx| { + this.selected_index = Some(event.index); + cx.notify(); + }, + )), + ) + .child(div().text_sm().text_color(muted).child(format!( + "Selected action index: {}", + self.selected_index + .map(|idx| idx.to_string()) + .unwrap_or_else(|| "".to_string()) + ))) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(560.), px(360.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| { + cx.new(|_| MenuExample { + selected_index: None, + }) + }, + ) + .unwrap(); + + cx.activate(true); + }); +} diff --git a/crates/gpui/examples/native_outline_view.rs b/crates/gpui/examples/native_outline_view.rs new file mode 100644 index 0000000..220e43f --- /dev/null +++ b/crates/gpui/examples/native_outline_view.rs @@ -0,0 +1,106 @@ +use gpui::{ + App, Application, Bounds, Context, NativeOutlineNode, OutlineRowSelectEvent, Window, + WindowAppearance, WindowBounds, WindowOptions, div, native_outline_view, prelude::*, px, rgb, + size, +}; + +struct OutlineViewExample { + selected_label: String, +} + +impl OutlineViewExample { + fn nodes() -> Vec { + vec![ + NativeOutlineNode::branch( + "Workspace", + vec![ + NativeOutlineNode::branch( + "Apps", + vec![ + NativeOutlineNode::leaf("Glass"), + NativeOutlineNode::leaf("Settings"), + NativeOutlineNode::leaf("Dashboard"), + ], + ), + NativeOutlineNode::branch( + "Services", + vec![ + NativeOutlineNode::leaf("Auth"), + NativeOutlineNode::leaf("Billing"), + NativeOutlineNode::leaf("Notifications"), + ], + ), + ], + ), + NativeOutlineNode::branch( + "Personal", + vec![ + NativeOutlineNode::leaf("Notes"), + NativeOutlineNode::leaf("Archive"), + ], + ), + ] + } +} + +impl Render for OutlineViewExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_dark = matches!( + window.appearance(), + WindowAppearance::Dark | WindowAppearance::VibrantDark + ); + let (bg, fg, muted) = if is_dark { + (rgb(0x171a21), rgb(0xffffff), rgb(0xb6bfce)) + } else { + (rgb(0xf5f7fc), rgb(0x1b2434), rgb(0x606b7d)) + }; + + div() + .flex() + .flex_col() + .size_full() + .gap_3() + .p_4() + .bg(bg) + .text_color(fg) + .child(div().text_xl().child("Native OutlineView (Tree)")) + .child( + native_outline_view("tree", &Self::nodes()) + .expand_all(true) + .row_height(24.0) + .on_select(cx.listener(|this, event: &OutlineRowSelectEvent, _, cx| { + this.selected_label = event.title.to_string(); + cx.notify(); + })) + .h(px(320.0)), + ) + .child(div().text_sm().text_color(muted).child(format!( + "Selected: {}", + if self.selected_label.is_empty() { + "".to_string() + } else { + self.selected_label.clone() + } + ))) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(720.), px(540.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| { + cx.new(|_| OutlineViewExample { + selected_label: String::new(), + }) + }, + ) + .unwrap(); + + cx.activate(true); + }); +} diff --git a/crates/gpui/examples/native_tab_view.rs b/crates/gpui/examples/native_tab_view.rs new file mode 100644 index 0000000..7fb75ac --- /dev/null +++ b/crates/gpui/examples/native_tab_view.rs @@ -0,0 +1,67 @@ +use gpui::{ + App, Application, Bounds, Context, TabSelectEvent, Window, WindowAppearance, WindowBounds, + WindowOptions, div, native_tab_view, prelude::*, px, rgb, size, +}; + +struct TabViewExample { + selected: usize, +} + +impl TabViewExample { + const LABELS: [&str; 5] = ["Overview", "Apps", "Settings", "Billing", "Logs"]; +} + +impl Render for TabViewExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_dark = matches!( + window.appearance(), + WindowAppearance::Dark | WindowAppearance::VibrantDark + ); + let (bg, fg, muted) = if is_dark { + (rgb(0x181c22), rgb(0xffffff), rgb(0xb5bdcb)) + } else { + (rgb(0xf4f7fb), rgb(0x1a2433), rgb(0x616b7b)) + }; + + div() + .flex() + .flex_col() + .size_full() + .gap_3() + .p_4() + .bg(bg) + .text_color(fg) + .child(div().text_xl().child("Native TabView")) + .child( + native_tab_view("tabs", &Self::LABELS) + .selected_index(self.selected) + .on_select(cx.listener(|this, event: &TabSelectEvent, _, cx| { + this.selected = event.index; + cx.notify(); + })) + .h(px(300.0)), + ) + .child( + div() + .text_sm() + .text_color(muted) + .child(format!("Selected: {}", Self::LABELS[self.selected])), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(680.), px(500.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| TabViewExample { selected: 0 }), + ) + .unwrap(); + + cx.activate(true); + }); +} diff --git a/crates/gpui/examples/native_table_styles.rs b/crates/gpui/examples/native_table_styles.rs new file mode 100644 index 0000000..9dfb918 --- /dev/null +++ b/crates/gpui/examples/native_table_styles.rs @@ -0,0 +1,237 @@ +use gpui::{ + App, Application, Bounds, CheckboxChangeEvent, Context, DropdownSelectEvent, + NativeTableGridMask, NativeTableRowSizeStyle, NativeTableSelectionHighlightStyle, + NativeTableStyle, TableRowSelectEvent, Window, WindowAppearance, WindowBounds, WindowOptions, + div, native_checkbox, native_dropdown, native_table_view, prelude::*, px, rgb, size, +}; + +struct TableStylesExample { + style_index: usize, + row_size_index: usize, + show_header: bool, + alternating_rows: bool, + selection_highlight: bool, + vertical_grid: bool, + horizontal_grid: bool, + dashed_horizontal_grid: bool, + selected: Option, +} + +impl TableStylesExample { + const STYLES: [&str; 5] = ["Automatic", "FullWidth", "Inset", "SourceList", "Plain"]; + const ROW_SIZES: [&str; 5] = ["Default", "Custom", "Small", "Medium", "Large"]; + const ROWS: [&str; 14] = [ + "Calendar", + "Mail", + "Notes", + "Music", + "Maps", + "Photos", + "Books", + "TV", + "Stocks", + "Weather", + "Shortcuts", + "Xcode", + "Terminal", + "Activity Monitor", + ]; + + fn table_style(&self) -> NativeTableStyle { + match self.style_index { + 1 => NativeTableStyle::FullWidth, + 2 => NativeTableStyle::Inset, + 3 => NativeTableStyle::SourceList, + 4 => NativeTableStyle::Plain, + _ => NativeTableStyle::Automatic, + } + } + + fn row_size_style(&self) -> NativeTableRowSizeStyle { + match self.row_size_index { + 0 => NativeTableRowSizeStyle::Default, + 1 => NativeTableRowSizeStyle::Custom, + 2 => NativeTableRowSizeStyle::Small, + 3 => NativeTableRowSizeStyle::Medium, + 4 => NativeTableRowSizeStyle::Large, + _ => NativeTableRowSizeStyle::Custom, + } + } + + fn grid_mask(&self) -> NativeTableGridMask { + let mut mask = NativeTableGridMask::NONE; + if self.vertical_grid { + mask = mask.union(NativeTableGridMask::SOLID_VERTICAL); + } + if self.horizontal_grid { + mask = mask.union(NativeTableGridMask::SOLID_HORIZONTAL); + } + if self.dashed_horizontal_grid { + mask = mask.union(NativeTableGridMask::DASHED_HORIZONTAL); + } + mask + } +} + +impl Render for TableStylesExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_dark = matches!( + window.appearance(), + WindowAppearance::Dark | WindowAppearance::VibrantDark + ); + let (bg, fg, muted) = if is_dark { + (rgb(0x191d24), rgb(0xffffff), rgb(0xb5bdcb)) + } else { + (rgb(0xf4f7fb), rgb(0x1a2434), rgb(0x5f6a7b)) + }; + + let table_style = self.table_style(); + let row_size_style = self.row_size_style(); + let selection_highlight_style = if self.selection_highlight { + NativeTableSelectionHighlightStyle::Regular + } else { + NativeTableSelectionHighlightStyle::None + }; + + div() + .flex() + .flex_col() + .size_full() + .gap_3() + .p_4() + .bg(bg) + .text_color(fg) + .child(div().text_xl().child("Native TableView Styles")) + .child( + div() + .flex() + .gap_3() + .items_center() + .child("Table style") + .child( + native_dropdown("style", &Self::STYLES) + .selected_index(self.style_index) + .on_select(cx.listener(|this, event: &DropdownSelectEvent, _, cx| { + this.style_index = event.index; + cx.notify(); + })) + .w(px(180.0)), + ) + .child("Row size") + .child( + native_dropdown("row_size", &Self::ROW_SIZES) + .selected_index(self.row_size_index) + .on_select(cx.listener(|this, event: &DropdownSelectEvent, _, cx| { + this.row_size_index = event.index; + cx.notify(); + })) + .w(px(160.0)), + ), + ) + .child( + div() + .flex() + .flex_wrap() + .gap_3() + .child( + native_checkbox("show_header", "Show header") + .checked(self.show_header) + .on_change(cx.listener(|this, event: &CheckboxChangeEvent, _, cx| { + this.show_header = event.checked; + cx.notify(); + })), + ) + .child( + native_checkbox("alternating", "Alternating rows") + .checked(self.alternating_rows) + .on_change(cx.listener(|this, event: &CheckboxChangeEvent, _, cx| { + this.alternating_rows = event.checked; + cx.notify(); + })), + ) + .child( + native_checkbox("highlight", "Selection highlight") + .checked(self.selection_highlight) + .on_change(cx.listener(|this, event: &CheckboxChangeEvent, _, cx| { + this.selection_highlight = event.checked; + cx.notify(); + })), + ) + .child( + native_checkbox("grid_v", "Vertical grid") + .checked(self.vertical_grid) + .on_change(cx.listener(|this, event: &CheckboxChangeEvent, _, cx| { + this.vertical_grid = event.checked; + cx.notify(); + })), + ) + .child( + native_checkbox("grid_h", "Horizontal grid") + .checked(self.horizontal_grid) + .on_change(cx.listener(|this, event: &CheckboxChangeEvent, _, cx| { + this.horizontal_grid = event.checked; + cx.notify(); + })), + ) + .child( + native_checkbox("grid_dh", "Dashed horizontal") + .checked(self.dashed_horizontal_grid) + .on_change(cx.listener(|this, event: &CheckboxChangeEvent, _, cx| { + this.dashed_horizontal_grid = event.checked; + cx.notify(); + })), + ), + ) + .child( + native_table_view("table", &Self::ROWS) + .table_style(table_style) + .row_size_style(row_size_style) + .selection_highlight_style(selection_highlight_style) + .column_title("Application") + .show_header(self.show_header) + .alternating_rows(self.alternating_rows) + .grid_mask(self.grid_mask()) + .selected_index(self.selected) + .row_height(24.0) + .on_select(cx.listener(|this, event: &TableRowSelectEvent, _, cx| { + this.selected = Some(event.index); + cx.notify(); + })) + .h(px(320.0)), + ) + .child(div().text_sm().text_color(muted).child(format!( + "Selected: {}", + self.selected + .map(|idx| Self::ROWS[idx].to_string()) + .unwrap_or_else(|| "".to_string()) + ))) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(920.), px(680.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| { + cx.new(|_| TableStylesExample { + style_index: 2, + row_size_index: 1, + show_header: true, + alternating_rows: true, + selection_highlight: true, + vertical_grid: false, + horizontal_grid: false, + dashed_horizontal_grid: false, + selected: None, + }) + }, + ) + .unwrap(); + + cx.activate(true); + }); +} diff --git a/crates/gpui/examples/native_table_view.rs b/crates/gpui/examples/native_table_view.rs new file mode 100644 index 0000000..c41c209 --- /dev/null +++ b/crates/gpui/examples/native_table_view.rs @@ -0,0 +1,83 @@ +use gpui::{ + App, Application, Bounds, Context, TableRowSelectEvent, Window, WindowAppearance, WindowBounds, + WindowOptions, div, native_table_view, prelude::*, px, rgb, size, +}; + +struct TableViewExample { + selected: Option, +} + +impl TableViewExample { + const ROWS: [&str; 14] = [ + "Aptos - Connected", + "Stripe - Connected", + "GitHub - Connected", + "Linear - Connected", + "Slack - Disconnected", + "Discord - Connected", + "Notion - Connected", + "Vercel - Connected", + "Sentry - Connected", + "Datadog - Disconnected", + "Dropbox - Connected", + "Figma - Connected", + "Jira - Connected", + "Confluence - Connected", + ]; +} + +impl Render for TableViewExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_dark = matches!( + window.appearance(), + WindowAppearance::Dark | WindowAppearance::VibrantDark + ); + let (bg, fg, muted) = if is_dark { + (rgb(0x171b20), rgb(0xffffff), rgb(0xb1b8c3)) + } else { + (rgb(0xf5f7fb), rgb(0x1a2332), rgb(0x5f6878)) + }; + + div() + .flex() + .flex_col() + .size_full() + .gap_3() + .p_4() + .bg(bg) + .text_color(fg) + .child(div().text_xl().child("Native TableView (Dense Rows)")) + .child( + native_table_view("connections", &Self::ROWS) + .selected_index(self.selected) + .row_height(24.0) + .on_select(cx.listener(|this, event: &TableRowSelectEvent, _, cx| { + this.selected = Some(event.index); + cx.notify(); + })) + .h(px(300.0)), + ) + .child(div().text_sm().text_color(muted).child(format!( + "Selected: {}", + self.selected + .map(|i| Self::ROWS[i].to_string()) + .unwrap_or_else(|| "".to_string()) + ))) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(700.), px(500.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| TableViewExample { selected: None }), + ) + .unwrap(); + + cx.activate(true); + }); +} diff --git a/crates/gpui/src/elements/mod.rs b/crates/gpui/src/elements/mod.rs index d7188dd..9939a8c 100644 --- a/crates/gpui/src/elements/mod.rs +++ b/crates/gpui/src/elements/mod.rs @@ -8,15 +8,20 @@ mod img; mod list; mod native_button; mod native_checkbox; +mod native_collection_view; mod native_combo_box; mod native_dropdown; mod native_element_helpers; mod native_icon_button; +mod native_menu_button; +mod native_outline_view; mod native_progress_bar; mod native_search_field; mod native_slider; mod native_stepper; mod native_switch; +mod native_tab_view; +mod native_table_view; mod native_text_field; mod native_toggle_group; mod surface; @@ -34,14 +39,19 @@ pub use img::*; pub use list::*; pub use native_button::*; pub use native_checkbox::*; +pub use native_collection_view::*; pub use native_combo_box::*; pub use native_dropdown::*; pub use native_icon_button::*; +pub use native_menu_button::*; +pub use native_outline_view::*; pub use native_progress_bar::*; pub use native_search_field::*; pub use native_slider::*; pub use native_stepper::*; pub use native_switch::*; +pub use native_tab_view::*; +pub use native_table_view::*; pub use native_text_field::*; pub use native_toggle_group::*; pub use surface::*; diff --git a/crates/gpui/src/elements/native_collection_view.rs b/crates/gpui/src/elements/native_collection_view.rs new file mode 100644 index 0000000..6622fc9 --- /dev/null +++ b/crates/gpui/src/elements/native_collection_view.rs @@ -0,0 +1,381 @@ +use refineable::Refineable as _; +use std::ffi::c_void; +use std::rc::Rc; + +use crate::{ + AbsoluteLength, App, Bounds, DefiniteLength, Element, ElementId, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, Length, Pixels, SharedString, Style, + StyleRefinement, Styled, Window, px, +}; + +use super::native_element_helpers::schedule_native_callback; + +/// Event emitted when a collection item (card) is clicked. +#[derive(Clone, Debug)] +pub struct CollectionSelectEvent { + /// Zero-based item index. + pub index: usize, +} + +/// Visual presentation for `NativeCollectionView` items. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum NativeCollectionItemStyle { + /// Plain list/grid labels with native selection. + Label, + /// Card-like cells backed by AppKit `NSBox`. + #[default] + Card, +} + +/// Creates a native collection view (NSCollectionView) with clickable cards. +pub fn native_collection_view( + id: impl Into, + items: &[impl AsRef], +) -> NativeCollectionView { + NativeCollectionView { + id: id.into(), + items: items + .iter() + .map(|item| SharedString::from(item.as_ref().to_string())) + .collect(), + selected_index: None, + columns: 2, + item_height: 72.0, + spacing: 8.0, + item_style: NativeCollectionItemStyle::default(), + on_select: None, + style: StyleRefinement::default(), + } +} + +/// A native NSCollectionView wrapper for clickable card/grid/list surfaces. +pub struct NativeCollectionView { + id: ElementId, + items: Vec, + selected_index: Option, + columns: usize, + item_height: f64, + spacing: f64, + item_style: NativeCollectionItemStyle, + on_select: Option>, + style: StyleRefinement, +} + +impl NativeCollectionView { + /// Sets the selected item index. + pub fn selected_index(mut self, selected_index: Option) -> Self { + self.selected_index = selected_index; + self + } + + /// Sets how many columns to render in the grid. + pub fn columns(mut self, columns: usize) -> Self { + self.columns = columns.max(1); + self + } + + /// Sets each card height in pixels. + pub fn item_height(mut self, item_height: f64) -> Self { + self.item_height = item_height.max(48.0); + self + } + + /// Sets spacing between cards in pixels. + pub fn spacing(mut self, spacing: f64) -> Self { + self.spacing = spacing.max(0.0); + self + } + + /// Sets how each item is rendered (`Label` or `Card`). + pub fn item_style(mut self, item_style: NativeCollectionItemStyle) -> Self { + self.item_style = item_style; + self + } + + /// Registers a callback fired when a card is clicked. + pub fn on_select( + mut self, + listener: impl Fn(&CollectionSelectEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_select = Some(Box::new(listener)); + self + } +} + +struct NativeCollectionViewState { + control_ptr: *mut c_void, + target_ptr: *mut c_void, + current_items: Vec, + current_selected: Option, + current_columns: usize, + current_width: f64, + current_item_height: f64, + current_spacing: f64, + current_item_style: NativeCollectionItemStyle, + attached: bool, +} + +impl Drop for NativeCollectionViewState { + fn drop(&mut self) { + if self.attached { + #[cfg(target_os = "macos")] + unsafe { + use crate::platform::native_controls; + super::native_element_helpers::cleanup_native_control( + self.control_ptr, + self.target_ptr, + native_controls::release_native_collection_target, + native_controls::release_native_collection_view, + ); + } + } + } +} + +unsafe impl Send for NativeCollectionViewState {} + +impl IntoElement for NativeCollectionView { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for NativeCollectionView { + type RequestLayoutState = (); + type PrepaintState = Bounds; + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + + if matches!(style.size.width, Length::Auto) { + style.size.width = + Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(420.0)))); + } + if matches!(style.size.height, Length::Auto) { + style.size.height = + Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(260.0)))); + } + + let layout_id = window.request_layout(style, [], cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Bounds { + bounds + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + _cx: &mut App, + ) { + #[cfg(target_os = "macos")] + { + use crate::platform::native_controls; + + let native_view = window.raw_native_view_ptr(); + if native_view.is_null() { + return; + } + + let mut on_select = self.on_select.take(); + let items = self.items.clone(); + let selected_index = self.selected_index; + let columns = self.columns; + let width = bounds.size.width.0 as f64; + let item_height = self.item_height; + let spacing = self.spacing; + let item_style = self.item_style; + + let next_frame_callbacks = window.next_frame_callbacks.clone(); + let invalidator = window.invalidator.clone(); + + window.with_optional_element_state::( + id, + |prev_state, window| { + let state = if let Some(Some(mut state)) = prev_state { + unsafe { + native_controls::set_native_view_frame( + state.control_ptr as cocoa::base::id, + bounds, + native_view as cocoa::base::id, + window.scale_factor(), + ); + } + + if state.current_columns != columns + || (state.current_width - width).abs() > f64::EPSILON + || state.current_item_height != item_height + || state.current_spacing != spacing + { + unsafe { + native_controls::set_native_collection_layout( + state.control_ptr as cocoa::base::id, + width, + columns, + item_height, + spacing, + ); + } + state.current_columns = columns; + state.current_width = width; + state.current_item_height = item_height; + state.current_spacing = spacing; + } + + let needs_rebind = state.current_items != items + || state.current_selected != selected_index + || state.current_item_style != item_style + || on_select.is_some(); + if needs_rebind { + unsafe { + native_controls::release_native_collection_target(state.target_ptr); + } + + let callback = on_select.take().map(|handler| { + let nfc = next_frame_callbacks.clone(); + let inv = invalidator.clone(); + let handler = Rc::new(handler); + schedule_native_callback( + handler, + |index| CollectionSelectEvent { index }, + nfc, + inv, + ) + }); + + let item_strs: Vec<&str> = + items.iter().map(|item| item.as_ref()).collect(); + unsafe { + state.target_ptr = + native_controls::set_native_collection_data_source( + state.control_ptr as cocoa::base::id, + &item_strs, + selected_index, + match item_style { + NativeCollectionItemStyle::Label => { + native_controls::NativeCollectionItemStyleData::Label + } + NativeCollectionItemStyle::Card => { + native_controls::NativeCollectionItemStyleData::Card + } + }, + callback, + ); + } + state.current_items = items.clone(); + state.current_selected = selected_index; + state.current_item_style = item_style; + } + + state + } else { + let callback = on_select.take().map(|handler| { + let nfc = next_frame_callbacks.clone(); + let inv = invalidator.clone(); + let handler = Rc::new(handler); + schedule_native_callback( + handler, + |index| CollectionSelectEvent { index }, + nfc, + inv, + ) + }); + + let (control_ptr, target_ptr) = unsafe { + let control = native_controls::create_native_collection_view(); + native_controls::set_native_collection_layout( + control, + width, + columns, + item_height, + spacing, + ); + + let item_strs: Vec<&str> = + items.iter().map(|item| item.as_ref()).collect(); + let target = native_controls::set_native_collection_data_source( + control, + &item_strs, + selected_index, + match item_style { + NativeCollectionItemStyle::Label => { + native_controls::NativeCollectionItemStyleData::Label + } + NativeCollectionItemStyle::Card => { + native_controls::NativeCollectionItemStyleData::Card + } + }, + callback, + ); + + native_controls::attach_native_view_to_parent( + control, + native_view as cocoa::base::id, + ); + native_controls::set_native_view_frame( + control, + bounds, + native_view as cocoa::base::id, + window.scale_factor(), + ); + + (control as *mut c_void, target) + }; + + NativeCollectionViewState { + control_ptr, + target_ptr, + current_items: items, + current_selected: selected_index, + current_columns: columns, + current_width: width, + current_item_height: item_height, + current_spacing: spacing, + current_item_style: item_style, + attached: true, + } + }; + + ((), Some(state)) + }, + ); + } + } +} + +impl Styled for NativeCollectionView { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} diff --git a/crates/gpui/src/elements/native_menu_button.rs b/crates/gpui/src/elements/native_menu_button.rs new file mode 100644 index 0000000..c305be7 --- /dev/null +++ b/crates/gpui/src/elements/native_menu_button.rs @@ -0,0 +1,413 @@ +use refineable::Refineable as _; +use std::ffi::c_void; +use std::rc::Rc; + +use crate::{ + AbsoluteLength, App, Bounds, DefiniteLength, Element, ElementId, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, Length, Pixels, SharedString, Style, + StyleRefinement, Styled, Window, px, +}; + +use super::native_element_helpers::schedule_native_callback; + +/// A declarative native menu item. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NativeMenuItem { + /// A clickable action item. + Action { + /// Visible title. + title: SharedString, + /// Whether this item is enabled. + enabled: bool, + }, + /// A submenu containing more menu items. + Submenu { + /// Visible title. + title: SharedString, + /// Whether this submenu is enabled. + enabled: bool, + /// Nested menu items. + items: Vec, + }, + /// A visual separator. + Separator, +} + +impl NativeMenuItem { + /// Creates an enabled action item. + pub fn action(title: impl Into) -> Self { + Self::Action { + title: title.into(), + enabled: true, + } + } + + /// Creates an enabled submenu. + pub fn submenu(title: impl Into, items: Vec) -> Self { + Self::Submenu { + title: title.into(), + enabled: true, + items, + } + } + + /// Creates a separator item. + pub fn separator() -> Self { + Self::Separator + } + + /// Sets enabled state on action and submenu items. + pub fn enabled(self, enabled: bool) -> Self { + match self { + Self::Action { title, .. } => Self::Action { title, enabled }, + Self::Submenu { title, items, .. } => Self::Submenu { + title, + enabled, + items, + }, + Self::Separator => Self::Separator, + } + } +} + +/// Event emitted when a menu action item is selected. +#[derive(Clone, Debug)] +pub struct MenuItemSelectEvent { + /// Zero-based action index across all action items (depth-first order). + pub index: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum NativeMenuKind { + Button, + Context, +} + +/// Creates a native menu button (NSButton + NSMenu/NSMenuItem). +pub fn native_menu_button( + id: impl Into, + label: impl Into, + items: &[NativeMenuItem], +) -> NativeMenuButton { + NativeMenuButton { + id: id.into(), + label: label.into(), + items: items.to_vec(), + on_select: None, + disabled: false, + kind: NativeMenuKind::Button, + style: StyleRefinement::default(), + } +} + +/// Creates a native context-menu trigger button. +/// +/// The menu opens on left click and right click. +pub fn native_context_menu( + id: impl Into, + label: impl Into, + items: &[NativeMenuItem], +) -> NativeMenuButton { + NativeMenuButton { + id: id.into(), + label: label.into(), + items: items.to_vec(), + on_select: None, + disabled: false, + kind: NativeMenuKind::Context, + style: StyleRefinement::default(), + } +} + +/// A native menu button/context menu element. +pub struct NativeMenuButton { + id: ElementId, + label: SharedString, + items: Vec, + on_select: Option>, + disabled: bool, + kind: NativeMenuKind, + style: StyleRefinement, +} + +impl NativeMenuButton { + /// Registers a callback for action item selection. + pub fn on_select( + mut self, + listener: impl Fn(&MenuItemSelectEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_select = Some(Box::new(listener)); + self + } + + /// Sets whether the trigger control is disabled. + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +struct NativeMenuButtonState { + control_ptr: *mut c_void, + target_ptr: *mut c_void, + current_label: SharedString, + current_items: Vec, + attached: bool, +} + +impl Drop for NativeMenuButtonState { + fn drop(&mut self) { + if self.attached { + #[cfg(target_os = "macos")] + unsafe { + use crate::platform::native_controls; + super::native_element_helpers::cleanup_native_control( + self.control_ptr, + self.target_ptr, + native_controls::release_native_menu_button_target, + native_controls::release_native_menu_button, + ); + } + } + } +} + +unsafe impl Send for NativeMenuButtonState {} + +#[cfg(target_os = "macos")] +fn map_items( + items: &[NativeMenuItem], +) -> Vec { + fn convert(item: &NativeMenuItem) -> crate::platform::native_controls::NativeMenuItemData { + match item { + NativeMenuItem::Action { title, enabled } => { + crate::platform::native_controls::NativeMenuItemData::Action { + title: title.to_string(), + enabled: *enabled, + } + } + NativeMenuItem::Submenu { + title, + enabled, + items, + } => crate::platform::native_controls::NativeMenuItemData::Submenu { + title: title.to_string(), + enabled: *enabled, + items: items.iter().map(convert).collect(), + }, + NativeMenuItem::Separator => { + crate::platform::native_controls::NativeMenuItemData::Separator + } + } + } + + items.iter().map(convert).collect() +} + +impl IntoElement for NativeMenuButton { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for NativeMenuButton { + type RequestLayoutState = (); + type PrepaintState = Bounds; + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + + if matches!(style.size.width, Length::Auto) { + let width = (self.label.len() as f32 * 8.0 + 40.0).max(140.0); + style.size.width = + Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(width)))); + } + if matches!(style.size.height, Length::Auto) { + style.size.height = + Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(26.0)))); + } + + let layout_id = window.request_layout(style, [], cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Bounds { + bounds + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + _cx: &mut App, + ) { + #[cfg(target_os = "macos")] + { + use crate::platform::native_controls; + + let native_view = window.raw_native_view_ptr(); + if native_view.is_null() { + return; + } + + let mut on_select = self.on_select.take(); + let label = self.label.clone(); + let items = self.items.clone(); + let disabled = self.disabled; + let kind = self.kind; + + let next_frame_callbacks = window.next_frame_callbacks.clone(); + let invalidator = window.invalidator.clone(); + + window.with_optional_element_state::( + id, + |prev_state, window| { + let state = if let Some(Some(mut state)) = prev_state { + unsafe { + native_controls::set_native_view_frame( + state.control_ptr as cocoa::base::id, + bounds, + native_view as cocoa::base::id, + window.scale_factor(), + ); + + native_controls::set_native_control_enabled( + state.control_ptr as cocoa::base::id, + !disabled, + ); + } + + if state.current_label != label { + unsafe { + native_controls::set_native_menu_button_title( + state.control_ptr as cocoa::base::id, + &label, + ); + } + state.current_label = label.clone(); + } + + if state.current_items != items || on_select.is_some() { + unsafe { + native_controls::release_native_menu_button_target( + state.target_ptr, + ); + } + + let callback = on_select.take().map(|handler| { + let nfc = next_frame_callbacks.clone(); + let inv = invalidator.clone(); + let handler = Rc::new(handler); + schedule_native_callback( + handler, + |index| MenuItemSelectEvent { index }, + nfc, + inv, + ) + }); + + let mapped = map_items(&items); + unsafe { + state.target_ptr = native_controls::set_native_menu_button_items( + state.control_ptr as cocoa::base::id, + &mapped, + callback, + ); + } + state.current_items = items.clone(); + } + + state + } else { + let callback = on_select.take().map(|handler| { + let nfc = next_frame_callbacks.clone(); + let inv = invalidator.clone(); + let handler = Rc::new(handler); + schedule_native_callback( + handler, + |index| MenuItemSelectEvent { index }, + nfc, + inv, + ) + }); + + let mapped = map_items(&items); + let (control_ptr, target_ptr) = unsafe { + let control = match kind { + NativeMenuKind::Button => { + native_controls::create_native_menu_button(&label) + } + NativeMenuKind::Context => { + native_controls::create_native_context_menu_button(&label) + } + }; + + native_controls::set_native_control_enabled(control, !disabled); + let target = native_controls::set_native_menu_button_items( + control, &mapped, callback, + ); + + native_controls::attach_native_view_to_parent( + control, + native_view as cocoa::base::id, + ); + native_controls::set_native_view_frame( + control, + bounds, + native_view as cocoa::base::id, + window.scale_factor(), + ); + + (control as *mut c_void, target) + }; + + NativeMenuButtonState { + control_ptr, + target_ptr, + current_label: label, + current_items: items, + attached: true, + } + }; + + ((), Some(state)) + }, + ); + } + } +} + +impl Styled for NativeMenuButton { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} diff --git a/crates/gpui/src/elements/native_outline_view.rs b/crates/gpui/src/elements/native_outline_view.rs new file mode 100644 index 0000000..1f45d2d --- /dev/null +++ b/crates/gpui/src/elements/native_outline_view.rs @@ -0,0 +1,363 @@ +use refineable::Refineable as _; +use std::ffi::c_void; +use std::rc::Rc; + +use crate::{ + AbsoluteLength, App, Bounds, DefiniteLength, Element, ElementId, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, Length, Pixels, SharedString, Style, + StyleRefinement, Styled, Window, px, +}; + +use super::native_element_helpers::schedule_native_callback; + +/// A node in a native outline tree. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NativeOutlineNode { + /// Label shown for this row. + pub title: SharedString, + /// Child nodes under this row. + pub children: Vec, +} + +impl NativeOutlineNode { + /// Creates a leaf node with no children. + pub fn leaf(title: impl Into) -> Self { + Self { + title: title.into(), + children: Vec::new(), + } + } + + /// Creates a node with children. + pub fn branch(title: impl Into, children: Vec) -> Self { + Self { + title: title.into(), + children, + } + } +} + +/// Event emitted when a row is selected in the outline. +#[derive(Clone, Debug)] +pub struct OutlineRowSelectEvent { + /// Zero-based row index in the currently visible outline rows. + pub index: usize, + /// Title of the selected row. + pub title: SharedString, +} + +/// Creates a native outline view (NSOutlineView) for tree/expandable lists. +pub fn native_outline_view( + id: impl Into, + nodes: &[NativeOutlineNode], +) -> NativeOutlineView { + NativeOutlineView { + id: id.into(), + nodes: nodes.to_vec(), + selected_row: None, + row_height: 22.0, + expand_all: true, + on_select: None, + style: StyleRefinement::default(), + } +} + +/// A native NSOutlineView wrapper for expandable hierarchical data. +pub struct NativeOutlineView { + id: ElementId, + nodes: Vec, + selected_row: Option, + row_height: f64, + expand_all: bool, + on_select: Option>, + style: StyleRefinement, +} + +impl NativeOutlineView { + /// Sets the selected visible row. + pub fn selected_row(mut self, selected_row: Option) -> Self { + self.selected_row = selected_row; + self + } + + /// Sets row height in pixels. + pub fn row_height(mut self, row_height: f64) -> Self { + self.row_height = row_height.max(16.0); + self + } + + /// Enables or disables expanding all nodes after reload. + pub fn expand_all(mut self, expand_all: bool) -> Self { + self.expand_all = expand_all; + self + } + + /// Registers a callback fired when a row is selected. + pub fn on_select( + mut self, + listener: impl Fn(&OutlineRowSelectEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_select = Some(Box::new(listener)); + self + } +} + +struct NativeOutlineViewState { + control_ptr: *mut c_void, + target_ptr: *mut c_void, + current_nodes: Vec, + current_selected_row: Option, + current_row_height: f64, + current_expand_all: bool, + attached: bool, +} + +impl Drop for NativeOutlineViewState { + fn drop(&mut self) { + if self.attached { + #[cfg(target_os = "macos")] + unsafe { + use crate::platform::native_controls; + super::native_element_helpers::cleanup_native_control( + self.control_ptr, + self.target_ptr, + native_controls::release_native_outline_target, + native_controls::release_native_outline_view, + ); + } + } + } +} + +unsafe impl Send for NativeOutlineViewState {} + +#[cfg(target_os = "macos")] +fn map_nodes( + nodes: &[NativeOutlineNode], +) -> Vec { + fn convert( + node: &NativeOutlineNode, + ) -> crate::platform::native_controls::NativeOutlineNodeData { + crate::platform::native_controls::NativeOutlineNodeData { + title: node.title.to_string(), + children: node.children.iter().map(convert).collect(), + } + } + + nodes.iter().map(convert).collect() +} + +impl IntoElement for NativeOutlineView { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for NativeOutlineView { + type RequestLayoutState = (); + type PrepaintState = Bounds; + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + + if matches!(style.size.width, Length::Auto) { + style.size.width = + Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(380.0)))); + } + if matches!(style.size.height, Length::Auto) { + style.size.height = + Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(260.0)))); + } + + let layout_id = window.request_layout(style, [], cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Bounds { + bounds + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + _cx: &mut App, + ) { + #[cfg(target_os = "macos")] + { + use crate::platform::native_controls; + + let native_view = window.raw_native_view_ptr(); + if native_view.is_null() { + return; + } + + let mut on_select = self.on_select.take(); + let nodes = self.nodes.clone(); + let selected_row = self.selected_row; + let row_height = self.row_height; + let expand_all = self.expand_all; + + let next_frame_callbacks = window.next_frame_callbacks.clone(); + let invalidator = window.invalidator.clone(); + + window.with_optional_element_state::( + id, + |prev_state, window| { + let state = if let Some(Some(mut state)) = prev_state { + unsafe { + native_controls::set_native_view_frame( + state.control_ptr as cocoa::base::id, + bounds, + native_view as cocoa::base::id, + window.scale_factor(), + ); + } + + if state.current_row_height != row_height { + unsafe { + native_controls::set_native_outline_row_height( + state.control_ptr as cocoa::base::id, + row_height, + ); + } + state.current_row_height = row_height; + } + + let needs_rebind = state.current_nodes != nodes + || state.current_selected_row != selected_row + || state.current_expand_all != expand_all + || on_select.is_some(); + if needs_rebind { + unsafe { + native_controls::release_native_outline_target(state.target_ptr); + } + + let callback = on_select.take().map(|handler| { + let nfc = next_frame_callbacks.clone(); + let inv = invalidator.clone(); + let handler = Rc::new(handler); + schedule_native_callback( + handler, + |(index, title): (usize, String)| OutlineRowSelectEvent { + index, + title: SharedString::from(title), + }, + nfc, + inv, + ) + }); + + let mapped = map_nodes(&nodes); + unsafe { + state.target_ptr = native_controls::set_native_outline_items( + state.control_ptr as cocoa::base::id, + &mapped, + selected_row, + expand_all, + callback, + ); + } + + state.current_nodes = nodes.clone(); + state.current_selected_row = selected_row; + state.current_expand_all = expand_all; + } + + state + } else { + let callback = on_select.take().map(|handler| { + let nfc = next_frame_callbacks.clone(); + let inv = invalidator.clone(); + let handler = Rc::new(handler); + schedule_native_callback( + handler, + |(index, title): (usize, String)| OutlineRowSelectEvent { + index, + title: SharedString::from(title), + }, + nfc, + inv, + ) + }); + + let mapped = map_nodes(&nodes); + + let (control_ptr, target_ptr) = unsafe { + let control = native_controls::create_native_outline_view(); + native_controls::set_native_outline_row_height(control, row_height); + + let target = native_controls::set_native_outline_items( + control, + &mapped, + selected_row, + expand_all, + callback, + ); + + native_controls::attach_native_view_to_parent( + control, + native_view as cocoa::base::id, + ); + native_controls::set_native_view_frame( + control, + bounds, + native_view as cocoa::base::id, + window.scale_factor(), + ); + + (control as *mut c_void, target) + }; + + NativeOutlineViewState { + control_ptr, + target_ptr, + current_nodes: nodes, + current_selected_row: selected_row, + current_row_height: row_height, + current_expand_all: expand_all, + attached: true, + } + }; + + ((), Some(state)) + }, + ); + } + } +} + +impl Styled for NativeOutlineView { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} diff --git a/crates/gpui/src/elements/native_tab_view.rs b/crates/gpui/src/elements/native_tab_view.rs new file mode 100644 index 0000000..4d5901c --- /dev/null +++ b/crates/gpui/src/elements/native_tab_view.rs @@ -0,0 +1,290 @@ +use refineable::Refineable as _; +use std::ffi::c_void; +use std::rc::Rc; + +use crate::{ + AbsoluteLength, App, Bounds, DefiniteLength, Element, ElementId, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, Length, Pixels, SharedString, Style, + StyleRefinement, Styled, Window, px, +}; + +use super::native_element_helpers::schedule_native_callback; + +/// Event emitted when a tab is selected in `NativeTabView`. +#[derive(Clone, Debug)] +pub struct TabSelectEvent { + /// Zero-based selected tab index. + pub index: usize, +} + +/// Creates a native tab view (NSTabView) for simple content tabs. +pub fn native_tab_view(id: impl Into, labels: &[impl AsRef]) -> NativeTabView { + NativeTabView { + id: id.into(), + labels: labels + .iter() + .map(|label| SharedString::from(label.as_ref().to_string())) + .collect(), + selected_index: 0, + on_select: None, + style: StyleRefinement::default(), + } +} + +/// A native NSTabView wrapper for simple tab navigation. +pub struct NativeTabView { + id: ElementId, + labels: Vec, + selected_index: usize, + on_select: Option>, + style: StyleRefinement, +} + +impl NativeTabView { + /// Sets the selected tab index. + pub fn selected_index(mut self, selected_index: usize) -> Self { + self.selected_index = selected_index; + self + } + + /// Registers a callback fired when tab selection changes. + pub fn on_select( + mut self, + listener: impl Fn(&TabSelectEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_select = Some(Box::new(listener)); + self + } +} + +struct NativeTabViewState { + control_ptr: *mut c_void, + target_ptr: *mut c_void, + current_labels: Vec, + current_selected: usize, + attached: bool, +} + +impl Drop for NativeTabViewState { + fn drop(&mut self) { + if self.attached { + #[cfg(target_os = "macos")] + unsafe { + use crate::platform::native_controls; + super::native_element_helpers::cleanup_native_control( + self.control_ptr, + self.target_ptr, + native_controls::release_native_tab_view_target, + native_controls::release_native_tab_view, + ); + } + } + } +} + +unsafe impl Send for NativeTabViewState {} + +impl IntoElement for NativeTabView { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for NativeTabView { + type RequestLayoutState = (); + type PrepaintState = Bounds; + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + + if matches!(style.size.width, Length::Auto) { + style.size.width = + Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(420.0)))); + } + if matches!(style.size.height, Length::Auto) { + style.size.height = + Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(280.0)))); + } + + let layout_id = window.request_layout(style, [], cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Bounds { + bounds + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + _cx: &mut App, + ) { + #[cfg(target_os = "macos")] + { + use crate::platform::native_controls; + + let native_view = window.raw_native_view_ptr(); + if native_view.is_null() { + return; + } + + let mut on_select = self.on_select.take(); + let labels = self.labels.clone(); + let selected_index = self.selected_index; + + let next_frame_callbacks = window.next_frame_callbacks.clone(); + let invalidator = window.invalidator.clone(); + + window.with_optional_element_state::( + id, + |prev_state, window| { + let state = if let Some(Some(mut state)) = prev_state { + unsafe { + native_controls::set_native_view_frame( + state.control_ptr as cocoa::base::id, + bounds, + native_view as cocoa::base::id, + window.scale_factor(), + ); + } + + if state.current_labels != labels { + let label_strs: Vec<&str> = + labels.iter().map(|label| label.as_ref()).collect(); + unsafe { + native_controls::set_native_tab_view_items( + state.control_ptr as cocoa::base::id, + &label_strs, + selected_index, + ); + } + state.current_labels = labels.clone(); + state.current_selected = selected_index; + } else if state.current_selected != selected_index { + unsafe { + native_controls::set_native_tab_view_selected( + state.control_ptr as cocoa::base::id, + selected_index, + ); + } + state.current_selected = selected_index; + } + + if on_select.is_some() { + unsafe { + native_controls::release_native_tab_view_target(state.target_ptr); + } + + let callback = on_select.take().map(|handler| { + let nfc = next_frame_callbacks.clone(); + let inv = invalidator.clone(); + let handler = Rc::new(handler); + schedule_native_callback( + handler, + |index| TabSelectEvent { index }, + nfc, + inv, + ) + }); + + unsafe { + state.target_ptr = native_controls::set_native_tab_view_action( + state.control_ptr as cocoa::base::id, + callback, + ); + } + } + + state + } else { + let callback = on_select.take().map(|handler| { + let nfc = next_frame_callbacks.clone(); + let inv = invalidator.clone(); + let handler = Rc::new(handler); + schedule_native_callback( + handler, + |index| TabSelectEvent { index }, + nfc, + inv, + ) + }); + + let (control_ptr, target_ptr) = unsafe { + let control = native_controls::create_native_tab_view(); + + let label_strs: Vec<&str> = + labels.iter().map(|label| label.as_ref()).collect(); + native_controls::set_native_tab_view_items( + control, + &label_strs, + selected_index, + ); + + let target = + native_controls::set_native_tab_view_action(control, callback); + + native_controls::attach_native_view_to_parent( + control, + native_view as cocoa::base::id, + ); + native_controls::set_native_view_frame( + control, + bounds, + native_view as cocoa::base::id, + window.scale_factor(), + ); + + (control as *mut c_void, target) + }; + + NativeTabViewState { + control_ptr, + target_ptr, + current_labels: labels, + current_selected: selected_index, + attached: true, + } + }; + + ((), Some(state)) + }, + ); + } + } +} + +impl Styled for NativeTabView { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} diff --git a/crates/gpui/src/elements/native_table_view.rs b/crates/gpui/src/elements/native_table_view.rs new file mode 100644 index 0000000..7825696 --- /dev/null +++ b/crates/gpui/src/elements/native_table_view.rs @@ -0,0 +1,604 @@ +use refineable::Refineable as _; +use std::ffi::c_void; +use std::rc::Rc; + +use crate::{ + AbsoluteLength, App, Bounds, DefiniteLength, Element, ElementId, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, Length, Pixels, SharedString, Style, + StyleRefinement, Styled, Window, px, +}; + +use super::native_element_helpers::schedule_native_callback; + +/// Event emitted when a table row is selected. +#[derive(Clone, Debug)] +pub struct TableRowSelectEvent { + /// Zero-based selected row index. + pub index: usize, +} + +/// AppKit `NSTableViewStyle`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum NativeTableStyle { + /// `NSTableViewStyleAutomatic`. + #[default] + Automatic, + /// `NSTableViewStyleFullWidth`. + FullWidth, + /// `NSTableViewStyleInset`. + Inset, + /// `NSTableViewStyleSourceList`. + SourceList, + /// `NSTableViewStylePlain`. + Plain, +} + +impl NativeTableStyle { + fn to_ns_style(self) -> i64 { + match self { + NativeTableStyle::Automatic => 0, + NativeTableStyle::FullWidth => 1, + NativeTableStyle::Inset => 2, + NativeTableStyle::SourceList => 3, + NativeTableStyle::Plain => 4, + } + } +} + +/// AppKit `NSTableViewRowSizeStyle`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum NativeTableRowSizeStyle { + /// `NSTableViewRowSizeStyleDefault`. + Default, + /// `NSTableViewRowSizeStyleCustom`. + #[default] + Custom, + /// `NSTableViewRowSizeStyleSmall`. + Small, + /// `NSTableViewRowSizeStyleMedium`. + Medium, + /// `NSTableViewRowSizeStyleLarge`. + Large, +} + +impl NativeTableRowSizeStyle { + fn to_ns_style(self) -> i64 { + match self { + NativeTableRowSizeStyle::Default => -1, + NativeTableRowSizeStyle::Custom => 0, + NativeTableRowSizeStyle::Small => 1, + NativeTableRowSizeStyle::Medium => 2, + NativeTableRowSizeStyle::Large => 3, + } + } +} + +/// AppKit `NSTableViewSelectionHighlightStyle`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum NativeTableSelectionHighlightStyle { + /// `NSTableViewSelectionHighlightStyleRegular`. + #[default] + Regular, + /// `NSTableViewSelectionHighlightStyleNone`. + None, +} + +impl NativeTableSelectionHighlightStyle { + fn to_ns_style(self) -> i64 { + match self { + NativeTableSelectionHighlightStyle::Regular => 0, + NativeTableSelectionHighlightStyle::None => -1, + } + } +} + +/// AppKit `NSTableViewGridLineStyle` bitmask. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NativeTableGridMask(u64); + +impl NativeTableGridMask { + /// `NSTableViewGridNone`. + pub const NONE: Self = Self(0); + /// `NSTableViewSolidVerticalGridLineMask`. + pub const SOLID_VERTICAL: Self = Self(1 << 0); + /// `NSTableViewSolidHorizontalGridLineMask`. + pub const SOLID_HORIZONTAL: Self = Self(1 << 1); + /// `NSTableViewDashedHorizontalGridLineMask`. + pub const DASHED_HORIZONTAL: Self = Self(1 << 3); + + /// Returns a new mask combining two grid styles. + pub fn union(self, other: Self) -> Self { + Self(self.0 | other.0) + } + + fn bits(self) -> u64 { + self.0 + } +} + +impl Default for NativeTableGridMask { + fn default() -> Self { + Self::NONE + } +} + +/// Creates a native table view (NSTableView) for dense row/list UIs. +pub fn native_table_view(id: impl Into, items: &[impl AsRef]) -> NativeTableView { + NativeTableView { + id: id.into(), + items: items + .iter() + .map(|item| SharedString::from(item.as_ref().to_string())) + .collect(), + selected_index: None, + row_height: 22.0, + column_title: SharedString::from("Value"), + show_header: false, + alternating_rows: true, + allows_multiple_selection: false, + table_style: NativeTableStyle::default(), + row_size_style: NativeTableRowSizeStyle::default(), + selection_highlight_style: NativeTableSelectionHighlightStyle::default(), + grid_mask: NativeTableGridMask::default(), + on_select: None, + style: StyleRefinement::default(), + } +} + +/// A native NSTableView wrapper with a single text column. +pub struct NativeTableView { + id: ElementId, + items: Vec, + selected_index: Option, + row_height: f64, + column_title: SharedString, + show_header: bool, + alternating_rows: bool, + allows_multiple_selection: bool, + table_style: NativeTableStyle, + row_size_style: NativeTableRowSizeStyle, + selection_highlight_style: NativeTableSelectionHighlightStyle, + grid_mask: NativeTableGridMask, + on_select: Option>, + style: StyleRefinement, +} + +impl NativeTableView { + /// Sets the selected row. + pub fn selected_index(mut self, selected_index: Option) -> Self { + self.selected_index = selected_index; + self + } + + /// Sets row height in pixels. + pub fn row_height(mut self, row_height: f64) -> Self { + self.row_height = row_height.max(16.0); + self + } + + /// Sets the single column title (used when header is shown). + pub fn column_title(mut self, column_title: impl Into) -> Self { + self.column_title = column_title.into(); + self + } + + /// Shows or hides the table header. + pub fn show_header(mut self, show_header: bool) -> Self { + self.show_header = show_header; + self + } + + /// Enables/disables alternating row backgrounds. + pub fn alternating_rows(mut self, alternating_rows: bool) -> Self { + self.alternating_rows = alternating_rows; + self + } + + /// Enables/disables multiple selection. + pub fn allows_multiple_selection(mut self, allows_multiple_selection: bool) -> Self { + self.allows_multiple_selection = allows_multiple_selection; + self + } + + /// Sets AppKit table style. + pub fn table_style(mut self, table_style: NativeTableStyle) -> Self { + self.table_style = table_style; + self + } + + /// Sets AppKit row size style. + pub fn row_size_style(mut self, row_size_style: NativeTableRowSizeStyle) -> Self { + self.row_size_style = row_size_style; + self + } + + /// Sets AppKit selection highlight style. + pub fn selection_highlight_style( + mut self, + selection_highlight_style: NativeTableSelectionHighlightStyle, + ) -> Self { + self.selection_highlight_style = selection_highlight_style; + self + } + + /// Sets AppKit grid line mask. + pub fn grid_mask(mut self, grid_mask: NativeTableGridMask) -> Self { + self.grid_mask = grid_mask; + self + } + + /// Registers a callback fired when the selection changes. + pub fn on_select( + mut self, + listener: impl Fn(&TableRowSelectEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_select = Some(Box::new(listener)); + self + } +} + +struct NativeTableViewState { + control_ptr: *mut c_void, + target_ptr: *mut c_void, + current_items: Vec, + current_selected: Option, + current_row_height: f64, + current_column_title: SharedString, + current_show_header: bool, + current_alternating_rows: bool, + current_allows_multiple_selection: bool, + current_table_style: NativeTableStyle, + current_row_size_style: NativeTableRowSizeStyle, + current_selection_highlight_style: NativeTableSelectionHighlightStyle, + current_grid_mask: NativeTableGridMask, + attached: bool, +} + +impl Drop for NativeTableViewState { + fn drop(&mut self) { + if self.attached { + #[cfg(target_os = "macos")] + unsafe { + use crate::platform::native_controls; + super::native_element_helpers::cleanup_native_control( + self.control_ptr, + self.target_ptr, + native_controls::release_native_table_target, + native_controls::release_native_table_view, + ); + } + } + } +} + +unsafe impl Send for NativeTableViewState {} + +impl IntoElement for NativeTableView { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for NativeTableView { + type RequestLayoutState = (); + type PrepaintState = Bounds; + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + + if matches!(style.size.width, Length::Auto) { + style.size.width = + Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(360.0)))); + } + if matches!(style.size.height, Length::Auto) { + style.size.height = + Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(240.0)))); + } + + let layout_id = window.request_layout(style, [], cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Bounds { + bounds + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + _cx: &mut App, + ) { + #[cfg(target_os = "macos")] + { + use crate::platform::native_controls; + + let native_view = window.raw_native_view_ptr(); + if native_view.is_null() { + return; + } + + let mut on_select = self.on_select.take(); + let items = self.items.clone(); + let selected_index = self.selected_index; + let row_height = self.row_height; + let column_title = self.column_title.clone(); + let show_header = self.show_header; + let alternating_rows = self.alternating_rows; + let allows_multiple_selection = self.allows_multiple_selection; + let table_style = self.table_style; + let row_size_style = self.row_size_style; + let selection_highlight_style = self.selection_highlight_style; + let grid_mask = self.grid_mask; + + let next_frame_callbacks = window.next_frame_callbacks.clone(); + let invalidator = window.invalidator.clone(); + + window.with_optional_element_state::( + id, + |prev_state, window| { + let state = if let Some(Some(mut state)) = prev_state { + unsafe { + native_controls::set_native_view_frame( + state.control_ptr as cocoa::base::id, + bounds, + native_view as cocoa::base::id, + window.scale_factor(), + ); + native_controls::set_native_table_column_width( + state.control_ptr as cocoa::base::id, + bounds.size.width.0 as f64, + ); + } + + if state.current_row_height != row_height { + unsafe { + native_controls::set_native_table_row_height( + state.control_ptr as cocoa::base::id, + row_height, + ); + } + state.current_row_height = row_height; + } + + if state.current_row_size_style != row_size_style { + unsafe { + native_controls::set_native_table_row_size_style( + state.control_ptr as cocoa::base::id, + row_size_style.to_ns_style(), + ); + } + state.current_row_size_style = row_size_style; + } + + if state.current_table_style != table_style { + unsafe { + native_controls::set_native_table_style( + state.control_ptr as cocoa::base::id, + table_style.to_ns_style(), + ); + } + state.current_table_style = table_style; + } + + if state.current_selection_highlight_style != selection_highlight_style { + unsafe { + native_controls::set_native_table_selection_highlight_style( + state.control_ptr as cocoa::base::id, + selection_highlight_style.to_ns_style(), + ); + } + state.current_selection_highlight_style = selection_highlight_style; + } + + if state.current_grid_mask != grid_mask { + unsafe { + native_controls::set_native_table_grid_style( + state.control_ptr as cocoa::base::id, + grid_mask.bits(), + ); + } + state.current_grid_mask = grid_mask; + } + + if state.current_alternating_rows != alternating_rows { + unsafe { + native_controls::set_native_table_uses_alternating_rows( + state.control_ptr as cocoa::base::id, + alternating_rows, + ); + } + state.current_alternating_rows = alternating_rows; + } + + if state.current_show_header != show_header { + unsafe { + native_controls::set_native_table_show_header( + state.control_ptr as cocoa::base::id, + show_header, + ); + } + state.current_show_header = show_header; + } + + if state.current_allows_multiple_selection != allows_multiple_selection { + unsafe { + native_controls::set_native_table_allows_multiple_selection( + state.control_ptr as cocoa::base::id, + allows_multiple_selection, + ); + } + state.current_allows_multiple_selection = allows_multiple_selection; + } + + if state.current_column_title != column_title { + unsafe { + native_controls::set_native_table_column_title( + state.control_ptr as cocoa::base::id, + &column_title, + ); + } + state.current_column_title = column_title.clone(); + } + + let needs_rebind = state.current_items != items + || state.current_selected != selected_index + || on_select.is_some(); + if needs_rebind { + unsafe { + native_controls::release_native_table_target(state.target_ptr); + } + + let callback = on_select.take().map(|handler| { + let nfc = next_frame_callbacks.clone(); + let inv = invalidator.clone(); + let handler = Rc::new(handler); + schedule_native_callback( + handler, + |index| TableRowSelectEvent { index }, + nfc, + inv, + ) + }); + + let item_strs: Vec<&str> = + items.iter().map(|item| item.as_ref()).collect(); + unsafe { + state.target_ptr = native_controls::set_native_table_items( + state.control_ptr as cocoa::base::id, + &item_strs, + selected_index, + callback, + ); + } + state.current_items = items.clone(); + state.current_selected = selected_index; + } + + state + } else { + let callback = on_select.take().map(|handler| { + let nfc = next_frame_callbacks.clone(); + let inv = invalidator.clone(); + let handler = Rc::new(handler); + schedule_native_callback( + handler, + |index| TableRowSelectEvent { index }, + nfc, + inv, + ) + }); + + let (control_ptr, target_ptr) = unsafe { + let control = native_controls::create_native_table_view(); + native_controls::set_native_table_column_width( + control, + bounds.size.width.0 as f64, + ); + native_controls::set_native_table_row_height(control, row_height); + native_controls::set_native_table_row_size_style( + control, + row_size_style.to_ns_style(), + ); + native_controls::set_native_table_style( + control, + table_style.to_ns_style(), + ); + native_controls::set_native_table_selection_highlight_style( + control, + selection_highlight_style.to_ns_style(), + ); + native_controls::set_native_table_grid_style(control, grid_mask.bits()); + native_controls::set_native_table_uses_alternating_rows( + control, + alternating_rows, + ); + native_controls::set_native_table_show_header(control, show_header); + native_controls::set_native_table_allows_multiple_selection( + control, + allows_multiple_selection, + ); + native_controls::set_native_table_column_title(control, &column_title); + + let item_strs: Vec<&str> = + items.iter().map(|item| item.as_ref()).collect(); + let target = native_controls::set_native_table_items( + control, + &item_strs, + selected_index, + callback, + ); + + native_controls::attach_native_view_to_parent( + control, + native_view as cocoa::base::id, + ); + native_controls::set_native_view_frame( + control, + bounds, + native_view as cocoa::base::id, + window.scale_factor(), + ); + + (control as *mut c_void, target) + }; + + NativeTableViewState { + control_ptr, + target_ptr, + current_items: items, + current_selected: selected_index, + current_row_height: row_height, + current_column_title: column_title, + current_show_header: show_header, + current_alternating_rows: alternating_rows, + current_allows_multiple_selection: allows_multiple_selection, + current_table_style: table_style, + current_row_size_style: row_size_style, + current_selection_highlight_style: selection_highlight_style, + current_grid_mask: grid_mask, + attached: true, + } + }; + + ((), Some(state)) + }, + ); + } + } +} + +impl Styled for NativeTableView { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} diff --git a/crates/gpui/src/platform/mac/native_controls/collection.rs b/crates/gpui/src/platform/mac/native_controls/collection.rs new file mode 100644 index 0000000..519d660 --- /dev/null +++ b/crates/gpui/src/platform/mac/native_controls/collection.rs @@ -0,0 +1,444 @@ +use super::CALLBACK_IVAR; +use cocoa::{ + base::{id, nil}, + foundation::{NSPoint, NSRect, NSSize}, +}; +use ctor::ctor; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use std::ffi::c_void; + +const COLLECTION_ITEM_IDENTIFIER: &str = "GPUINativeCollectionItem"; +const ITEM_CARD_TAG: i64 = 1001; +const ITEM_LABEL_TAG: i64 = 1002; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum NativeCollectionItemStyleData { + Label, + Card, +} + +struct CollectionCallbacks { + items: Vec, + selected: Option, + item_style: NativeCollectionItemStyleData, + on_select: Option>, +} + +static mut COLLECTION_DELEGATE_CLASS: *const Class = std::ptr::null(); + +#[ctor] +unsafe fn build_collection_delegate_class() { + unsafe { + let mut decl = ClassDecl::new("GPUINativeCollectionDelegate", class!(NSObject)).unwrap(); + decl.add_ivar::<*mut c_void>(CALLBACK_IVAR); + + decl.add_method( + sel!(collectionView:numberOfItemsInSection:), + collection_number_of_items as extern "C" fn(&Object, Sel, id, i64) -> i64, + ); + decl.add_method( + sel!(collectionView:itemForRepresentedObjectAtIndexPath:), + collection_item_for_index_path as extern "C" fn(&Object, Sel, id, id) -> id, + ); + decl.add_method( + sel!(collectionView:didSelectItemsAtIndexPaths:), + collection_did_select_items_at_index_paths as extern "C" fn(&Object, Sel, id, id), + ); + + COLLECTION_DELEGATE_CLASS = decl.register(); + } +} + +extern "C" fn collection_number_of_items( + this: &Object, + _sel: Sel, + _view: id, + _section: i64, +) -> i64 { + unsafe { + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return 0; + } + let callbacks = &*(ptr as *const CollectionCallbacks); + callbacks.items.len() as i64 + } +} + +unsafe fn find_subview_with_tag(parent: id, tag: i64) -> id { + unsafe { msg_send![parent, viewWithTag: tag] } +} + +unsafe fn ensure_collection_item_view(item: id) -> id { + unsafe { + let current_view: id = msg_send![item, view]; + if current_view != nil { + return current_view; + } + + let view: id = msg_send![class!(NSView), alloc]; + let view: id = msg_send![view, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(160.0, 72.0), + )]; + let _: () = msg_send![view, setAutoresizingMask: 0u64]; + + let _: () = msg_send![item, setView: view]; + let _: () = msg_send![view, release]; + + view + } +} + +unsafe fn ensure_label(parent: id, tag: i64) -> id { + unsafe { + let existing = find_subview_with_tag(parent, tag); + if existing != nil { + return existing; + } + + let label: id = + msg_send![class!(NSTextField), labelWithString: super::super::ns_string("")]; + let _: () = msg_send![label, setTag: tag]; + // NSTextAlignmentCenter = 2 + let _: () = msg_send![label, setAlignment: 2u64]; + let _: () = msg_send![label, setAutoresizingMask: 18u64]; // width + height sizable + let _: () = msg_send![parent, addSubview: label]; + + label + } +} + +unsafe fn ensure_card_field(parent: id, tag: i64) -> id { + unsafe { + let existing = find_subview_with_tag(parent, tag); + if existing != nil { + return existing; + } + + let field: id = msg_send![class!(NSTextField), alloc]; + let field: id = msg_send![field, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(160.0, 72.0), + )]; + let _: () = msg_send![field, setTag: tag]; + let _: () = msg_send![field, setEditable: 0i8]; + let _: () = msg_send![field, setSelectable: 0i8]; + let _: () = msg_send![field, setBezeled: 1i8]; + let _: () = msg_send![field, setBordered: 1i8]; + let _: () = msg_send![field, setDrawsBackground: 1i8]; + let _: () = msg_send![field, setAlignment: 2u64]; + let _: () = msg_send![field, setAutoresizingMask: 18u64]; + let _: () = msg_send![parent, addSubview: field]; + let _: () = msg_send![field, release]; + + field + } +} + +unsafe fn configure_collection_item_label(item: id, item_view: id, title: &str, selected: bool) { + unsafe { + let card = find_subview_with_tag(item_view, ITEM_CARD_TAG); + if card != nil { + let _: () = msg_send![card, setHidden: 1i8]; + } + + let label = ensure_label(item_view, ITEM_LABEL_TAG); + let _: () = msg_send![label, setHidden: 0i8]; + + let bounds: NSRect = msg_send![item_view, bounds]; + let _: () = msg_send![label, setFrame: bounds]; + let _: () = msg_send![label, setStringValue: super::super::ns_string(title)]; + + let color: id = if selected { + msg_send![class!(NSColor), selectedTextColor] + } else { + msg_send![class!(NSColor), labelColor] + }; + let _: () = msg_send![label, setTextColor: color]; + + let _: () = msg_send![item, setTextField: label]; + } +} + +unsafe fn configure_collection_item_card(item: id, item_view: id, title: &str, selected: bool) { + unsafe { + let plain_label = find_subview_with_tag(item_view, ITEM_LABEL_TAG); + if plain_label != nil { + let _: () = msg_send![plain_label, setHidden: 1i8]; + } + + let card_field = ensure_card_field(item_view, ITEM_CARD_TAG); + let _: () = msg_send![card_field, setHidden: 0i8]; + + let bounds: NSRect = msg_send![item_view, bounds]; + let frame = NSRect::new( + NSPoint::new(4.0, 4.0), + NSSize::new( + (bounds.size.width - 8.0).max(1.0), + (bounds.size.height - 8.0).max(1.0), + ), + ); + let _: () = msg_send![card_field, setFrame: frame]; + let _: () = msg_send![card_field, setStringValue: super::super::ns_string(title)]; + + let (bg, text): (id, id) = if selected { + ( + msg_send![class!(NSColor), selectedControlColor], + msg_send![class!(NSColor), alternateSelectedControlTextColor], + ) + } else { + ( + msg_send![class!(NSColor), controlBackgroundColor], + msg_send![class!(NSColor), labelColor], + ) + }; + let _: () = msg_send![card_field, setBackgroundColor: bg]; + let _: () = msg_send![card_field, setTextColor: text]; + let _: () = msg_send![item, setTextField: card_field]; + } +} + +extern "C" fn collection_item_for_index_path( + this: &Object, + _sel: Sel, + collection_view: id, + index_path: id, +) -> id { + unsafe { + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return nil; + } + + let identifier = super::super::ns_string(COLLECTION_ITEM_IDENTIFIER); + let item: id = msg_send![ + collection_view, + makeItemWithIdentifier: identifier + forIndexPath: index_path + ]; + if item == nil { + return nil; + } + + let callbacks = &*(ptr as *const CollectionCallbacks); + let index: i64 = msg_send![index_path, item]; + if index < 0 || (index as usize) >= callbacks.items.len() { + return item; + } + + let item_view = ensure_collection_item_view(item); + let title = &callbacks.items[index as usize]; + let selected = callbacks.selected == Some(index as usize); + match callbacks.item_style { + NativeCollectionItemStyleData::Label => { + configure_collection_item_label(item, item_view, title, selected) + } + NativeCollectionItemStyleData::Card => { + configure_collection_item_card(item, item_view, title, selected) + } + } + + item + } +} + +extern "C" fn collection_did_select_items_at_index_paths( + this: &Object, + _sel: Sel, + _collection_view: id, + index_paths: id, +) { + unsafe { + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return; + } + let callbacks = &*(ptr as *const CollectionCallbacks); + if let Some(ref on_select) = callbacks.on_select { + let any_path: id = msg_send![index_paths, anyObject]; + if any_path != nil { + let index: i64 = msg_send![any_path, item]; + if index >= 0 { + on_select(index as usize); + } + } + } + } +} + +unsafe fn flow_layout_from_collection(collection: id) -> id { + unsafe { + let layout: id = msg_send![collection, collectionViewLayout]; + let flow_layout_class = class!(NSCollectionViewFlowLayout); + let is_flow: i8 = msg_send![layout, isKindOfClass: flow_layout_class]; + if is_flow != 0 { layout } else { nil } + } +} + +unsafe fn set_flow_layout_spacing(layout: id, spacing: f64) { + unsafe { + let _: () = msg_send![layout, setMinimumInteritemSpacing: spacing]; + let _: () = msg_send![layout, setMinimumLineSpacing: spacing]; + } +} + +unsafe fn set_flow_layout_item_size(layout: id, size: NSSize) { + unsafe { + let _: () = msg_send![layout, setItemSize: size]; + } +} + +unsafe fn set_flow_layout_scroll_direction(layout: id, direction: i64) { + unsafe { + let _: () = msg_send![layout, setScrollDirection: direction]; + } +} + +pub(crate) unsafe fn create_native_collection_view() -> id { + unsafe { + let layout: id = msg_send![class!(NSCollectionViewFlowLayout), alloc]; + let layout: id = msg_send![layout, init]; + set_flow_layout_scroll_direction(layout, 0i64); + set_flow_layout_spacing(layout, 8.0); + + let collection: id = msg_send![class!(NSCollectionView), alloc]; + let collection: id = msg_send![collection, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(320.0, 200.0), + )]; + let _: () = msg_send![collection, setCollectionViewLayout: layout]; + let _: () = msg_send![collection, setSelectable: 1i8]; + let _: () = msg_send![collection, setAllowsEmptySelection: 1i8]; + let _: () = msg_send![collection, setAllowsMultipleSelection: 0i8]; + let _: () = msg_send![collection, setAutoresizingMask: 0u64]; + + let identifier = super::super::ns_string(COLLECTION_ITEM_IDENTIFIER); + let _: () = msg_send![collection, registerClass: class!(NSCollectionViewItem) forItemWithIdentifier: identifier]; + + let scroll: id = msg_send![class!(NSScrollView), alloc]; + let scroll: id = msg_send![scroll, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(320.0, 200.0), + )]; + let _: () = msg_send![scroll, setHasVerticalScroller: 1i8]; + let _: () = msg_send![scroll, setHasHorizontalScroller: 0i8]; + let _: () = msg_send![scroll, setBorderType: 1u64]; + let _: () = msg_send![scroll, setDocumentView: collection]; + let _: () = msg_send![scroll, setAutoresizingMask: 0u64]; + + let _: () = msg_send![layout, release]; + scroll + } +} + +pub(crate) unsafe fn set_native_collection_layout( + scroll_view: id, + width: f64, + columns: usize, + item_height: f64, + spacing: f64, +) { + unsafe { + let collection = collection_from_scroll(scroll_view); + let layout = flow_layout_from_collection(collection); + if layout == nil { + return; + } + + let columns = columns.max(1) as f64; + let spacing = spacing.max(0.0); + // Reserve conservative horizontal padding to avoid invalid-size warnings. + let usable_width = (width - 24.0).max(80.0); + let total_spacing = spacing * (columns - 1.0); + let item_width = ((usable_width - total_spacing) / columns).max(80.0); + + set_flow_layout_spacing(layout, spacing); + + let size = NSSize::new(item_width, item_height.max(48.0)); + set_flow_layout_item_size(layout, size); + } +} + +unsafe fn collection_from_scroll(scroll_view: id) -> id { + unsafe { msg_send![scroll_view, documentView] } +} + +unsafe fn apply_collection_selected(collection: id, selected: Option, len: usize) { + unsafe { + let index_paths: id = msg_send![class!(NSMutableSet), set]; + + if let Some(index) = selected { + if index < len { + let index_path: id = + msg_send![class!(NSIndexPath), indexPathForItem: index as i64 inSection: 0i64]; + let _: () = msg_send![index_paths, addObject: index_path]; + } + } + + let _: () = msg_send![collection, setSelectionIndexPaths: index_paths]; + } +} + +pub(crate) unsafe fn set_native_collection_data_source( + scroll_view: id, + items: &[&str], + selected: Option, + item_style: NativeCollectionItemStyleData, + on_select: Option>, +) -> *mut c_void { + unsafe { + let collection = collection_from_scroll(scroll_view); + + let delegate: id = msg_send![COLLECTION_DELEGATE_CLASS, alloc]; + let delegate: id = msg_send![delegate, init]; + + let callbacks = CollectionCallbacks { + items: items.iter().map(|item| item.to_string()).collect(), + selected, + item_style, + on_select, + }; + + let callbacks_ptr = Box::into_raw(Box::new(callbacks)) as *mut c_void; + (*delegate).set_ivar::<*mut c_void>(CALLBACK_IVAR, callbacks_ptr); + + let _: () = msg_send![collection, setDataSource: delegate]; + let _: () = msg_send![collection, setDelegate: delegate]; + let _: () = msg_send![collection, reloadData]; + + apply_collection_selected(collection, selected, items.len()); + + delegate as *mut c_void + } +} + +pub(crate) unsafe fn release_native_collection_target(target: *mut c_void) { + unsafe { + if target.is_null() { + return; + } + + let delegate = target as id; + let callbacks_ptr: *mut c_void = *(*delegate).get_ivar(CALLBACK_IVAR); + if !callbacks_ptr.is_null() { + let _ = Box::from_raw(callbacks_ptr as *mut CollectionCallbacks); + } + let _: () = msg_send![delegate, release]; + } +} + +pub(crate) unsafe fn release_native_collection_view(scroll_view: id) { + unsafe { + let collection = collection_from_scroll(scroll_view); + let _: () = msg_send![collection, setDataSource: nil]; + let _: () = msg_send![collection, setDelegate: nil]; + let _: () = msg_send![scroll_view, release]; + } +} diff --git a/crates/gpui/src/platform/mac/native_controls/menu.rs b/crates/gpui/src/platform/mac/native_controls/menu.rs new file mode 100644 index 0000000..75084ce --- /dev/null +++ b/crates/gpui/src/platform/mac/native_controls/menu.rs @@ -0,0 +1,265 @@ +use super::CALLBACK_IVAR; +use cocoa::{ + base::{id, nil}, + foundation::{NSPoint, NSRect, NSSize}, +}; +use ctor::ctor; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use std::{ffi::c_void, ptr}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum NativeMenuItemData { + Action { + title: String, + enabled: bool, + }, + Submenu { + title: String, + enabled: bool, + items: Vec, + }, + Separator, +} + +struct MenuCallbacks { + menu: id, + on_select: Option>, +} + +impl Drop for MenuCallbacks { + fn drop(&mut self) { + unsafe { + if self.menu != nil { + let _: () = msg_send![self.menu, release]; + } + } + } +} + +static mut MENU_TARGET_CLASS: *const Class = ptr::null(); +static mut CONTEXT_BUTTON_CLASS: *const Class = ptr::null(); + +#[ctor] +unsafe fn build_menu_classes() { + unsafe { + let mut target_decl = ClassDecl::new("GPUINativeMenuTarget", class!(NSObject)).unwrap(); + target_decl.add_ivar::<*mut c_void>(CALLBACK_IVAR); + target_decl.add_method( + sel!(menuButtonAction:), + menu_button_action as extern "C" fn(&Object, Sel, id), + ); + target_decl.add_method( + sel!(menuItemAction:), + menu_item_action as extern "C" fn(&Object, Sel, id), + ); + MENU_TARGET_CLASS = target_decl.register(); + } + + unsafe { + let mut context_decl = + ClassDecl::new("GPUINativeContextMenuButton", class!(NSButton)).unwrap(); + context_decl.add_method( + sel!(rightMouseDown:), + context_right_mouse_down as extern "C" fn(&Object, Sel, id), + ); + CONTEXT_BUTTON_CLASS = context_decl.register(); + } +} + +extern "C" fn menu_button_action(this: &Object, _sel: Sel, sender: id) { + unsafe { + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return; + } + let callbacks = &*(ptr as *const MenuCallbacks); + + let point = NSPoint::new(0.0, 0.0); + let _: i8 = msg_send![ + callbacks.menu, + popUpMenuPositioningItem: nil + atLocation: point + inView: sender + ]; + } +} + +extern "C" fn menu_item_action(this: &Object, _sel: Sel, sender: id) { + unsafe { + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return; + } + let callbacks = &*(ptr as *const MenuCallbacks); + if let Some(ref on_select) = callbacks.on_select { + let tag: i64 = msg_send![sender, tag]; + if tag >= 0 { + on_select(tag as usize); + } + } + } +} + +extern "C" fn context_right_mouse_down(this: &Object, _sel: Sel, _event: id) { + unsafe { + let action: Sel = msg_send![this, action]; + let target: id = msg_send![this, target]; + let _: () = msg_send![this, sendAction: action to: target]; + } +} + +unsafe fn build_menu( + title: &str, + items: &[NativeMenuItemData], + target: id, + next_action_index: &mut usize, +) -> id { + unsafe { + use super::super::ns_string; + + let menu: id = msg_send![class!(NSMenu), alloc]; + let menu: id = msg_send![menu, initWithTitle: ns_string(title)]; + // Keep menu item enabled state explicit; avoid AppKit auto-validation + // disabling submenu items without a target/action. + let _: () = msg_send![menu, setAutoenablesItems: 0i8]; + + for item in items { + match item { + NativeMenuItemData::Separator => { + let sep: id = msg_send![class!(NSMenuItem), separatorItem]; + let _: () = msg_send![menu, addItem: sep]; + } + NativeMenuItemData::Action { title, enabled } => { + let menu_item: id = msg_send![class!(NSMenuItem), alloc]; + let menu_item: id = msg_send![ + menu_item, + initWithTitle: ns_string(title) + action: sel!(menuItemAction:) + keyEquivalent: ns_string("") + ]; + let _: () = msg_send![menu_item, setTag: *next_action_index as i64]; + let _: () = msg_send![menu_item, setTarget: target]; + let _: () = msg_send![menu_item, setEnabled: *enabled as i8]; + let _: () = msg_send![menu, addItem: menu_item]; + let _: () = msg_send![menu_item, release]; + *next_action_index += 1; + } + NativeMenuItemData::Submenu { + title, + enabled, + items, + } => { + let submenu = build_menu(title, items, target, next_action_index); + + let parent_item: id = msg_send![class!(NSMenuItem), alloc]; + let null_sel: Sel = std::mem::transmute(0usize); + let parent_item: id = msg_send![ + parent_item, + initWithTitle: ns_string(title) + action: null_sel + keyEquivalent: ns_string("") + ]; + let _: () = msg_send![parent_item, setEnabled: *enabled as i8]; + let _: () = msg_send![parent_item, setTarget: nil]; + let _: () = msg_send![parent_item, setSubmenu: submenu]; + let _: () = msg_send![menu, addItem: parent_item]; + + let _: () = msg_send![submenu, release]; + let _: () = msg_send![parent_item, release]; + } + } + } + + menu + } +} + +pub(crate) unsafe fn create_native_menu_button(title: &str) -> id { + unsafe { + use super::super::ns_string; + + let button: id = msg_send![class!(NSButton), alloc]; + let button: id = msg_send![button, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(140.0, 24.0), + )]; + let _: () = msg_send![button, setTitle: ns_string(title)]; + let _: () = msg_send![button, setBezelStyle: 1i64]; + let _: () = msg_send![button, setAutoresizingMask: 0u64]; + button + } +} + +pub(crate) unsafe fn create_native_context_menu_button(title: &str) -> id { + unsafe { + use super::super::ns_string; + + let button: id = msg_send![CONTEXT_BUTTON_CLASS, alloc]; + let button: id = msg_send![button, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(180.0, 26.0), + )]; + let _: () = msg_send![button, setTitle: ns_string(title)]; + let _: () = msg_send![button, setBezelStyle: 1i64]; + let _: () = msg_send![button, setAutoresizingMask: 0u64]; + button + } +} + +pub(crate) unsafe fn set_native_menu_button_title(button: id, title: &str) { + unsafe { + use super::super::ns_string; + let _: () = msg_send![button, setTitle: ns_string(title)]; + } +} + +pub(crate) unsafe fn set_native_menu_button_items( + button: id, + items: &[NativeMenuItemData], + on_select: Option>, +) -> *mut c_void { + unsafe { + let target: id = msg_send![MENU_TARGET_CLASS, alloc]; + let target: id = msg_send![target, init]; + + let mut next_action_index = 0; + let menu = build_menu("menu", items, target, &mut next_action_index); + + let callbacks = MenuCallbacks { menu, on_select }; + let callbacks_ptr = Box::into_raw(Box::new(callbacks)) as *mut c_void; + (*target).set_ivar::<*mut c_void>(CALLBACK_IVAR, callbacks_ptr); + + let _: () = msg_send![button, setTarget: target]; + let _: () = msg_send![button, setAction: sel!(menuButtonAction:)]; + + target as *mut c_void + } +} + +pub(crate) unsafe fn release_native_menu_button_target(target: *mut c_void) { + unsafe { + if target.is_null() { + return; + } + + let target = target as id; + let callbacks_ptr: *mut c_void = *(*target).get_ivar(CALLBACK_IVAR); + if !callbacks_ptr.is_null() { + let _ = Box::from_raw(callbacks_ptr as *mut MenuCallbacks); + } + + let _: () = msg_send![target, release]; + } +} + +pub(crate) unsafe fn release_native_menu_button(button: id) { + unsafe { + let _: () = msg_send![button, release]; + } +} diff --git a/crates/gpui/src/platform/mac/native_controls/mod.rs b/crates/gpui/src/platform/mac/native_controls/mod.rs index d1cfbf2..96d11a3 100644 --- a/crates/gpui/src/platform/mac/native_controls/mod.rs +++ b/crates/gpui/src/platform/mac/native_controls/mod.rs @@ -1,6 +1,9 @@ mod button; mod checkbox; +mod collection; mod combo_box; +mod menu; +mod outline; mod popup; mod progress; mod search_field; @@ -8,11 +11,16 @@ mod segmented; mod slider; mod stepper; mod switch; +mod tab_view; +mod table; mod text_field; pub(crate) use button::*; pub(crate) use checkbox::*; +pub(crate) use collection::*; pub(crate) use combo_box::*; +pub(crate) use menu::*; +pub(crate) use outline::*; pub(crate) use popup::*; pub(crate) use progress::*; pub(crate) use search_field::*; @@ -20,6 +28,8 @@ pub(crate) use segmented::*; pub(crate) use slider::*; pub(crate) use stepper::*; pub(crate) use switch::*; +pub(crate) use tab_view::*; +pub(crate) use table::*; pub(crate) use text_field::*; use crate::{Bounds, Pixels}; diff --git a/crates/gpui/src/platform/mac/native_controls/outline.rs b/crates/gpui/src/platform/mac/native_controls/outline.rs new file mode 100644 index 0000000..bda7f3f --- /dev/null +++ b/crates/gpui/src/platform/mac/native_controls/outline.rs @@ -0,0 +1,327 @@ +use super::CALLBACK_IVAR; +use cocoa::{ + base::{id, nil}, + foundation::{NSPoint, NSRect, NSSize}, +}; +use ctor::ctor; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use std::{ffi::c_void, ptr}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct NativeOutlineNodeData { + pub title: String, + pub children: Vec, +} + +struct OutlineCallbacks { + roots: id, + on_select: Option>, +} + +impl Drop for OutlineCallbacks { + fn drop(&mut self) { + unsafe { + if self.roots != nil { + let _: () = msg_send![self.roots, release]; + } + } + } +} + +static mut OUTLINE_DELEGATE_CLASS: *const Class = ptr::null(); + +#[ctor] +unsafe fn build_outline_delegate_class() { + unsafe { + let mut decl = ClassDecl::new("GPUINativeOutlineDelegate", class!(NSObject)).unwrap(); + decl.add_ivar::<*mut c_void>(CALLBACK_IVAR); + + decl.add_method( + sel!(outlineView:numberOfChildrenOfItem:), + number_of_children as extern "C" fn(&Object, Sel, id, id) -> i64, + ); + decl.add_method( + sel!(outlineView:isItemExpandable:), + is_item_expandable as extern "C" fn(&Object, Sel, id, id) -> i8, + ); + decl.add_method( + sel!(outlineView:child:ofItem:), + child_of_item as extern "C" fn(&Object, Sel, id, i64, id) -> id, + ); + decl.add_method( + sel!(outlineView:objectValueForTableColumn:byItem:), + object_value_for_item as extern "C" fn(&Object, Sel, id, id, id) -> id, + ); + decl.add_method( + sel!(outlineViewSelectionDidChange:), + selection_did_change as extern "C" fn(&Object, Sel, id), + ); + + OUTLINE_DELEGATE_CLASS = decl.register(); + } +} + +unsafe fn string_from_ns_string(ns_string: id) -> String { + unsafe { + let cstr: *const std::os::raw::c_char = msg_send![ns_string, UTF8String]; + if cstr.is_null() { + String::new() + } else { + std::ffi::CStr::from_ptr(cstr) + .to_string_lossy() + .into_owned() + } + } +} + +unsafe fn children_array(roots: id, item: id) -> id { + unsafe { + use super::super::ns_string; + + if item == nil { + roots + } else { + msg_send![item, objectForKey: ns_string("children")] + } + } +} + +extern "C" fn number_of_children(this: &Object, _sel: Sel, _outline: id, item: id) -> i64 { + unsafe { + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return 0; + } + + let callbacks = &*(ptr as *const OutlineCallbacks); + let children = children_array(callbacks.roots, item); + let count: u64 = msg_send![children, count]; + count as i64 + } +} + +extern "C" fn is_item_expandable(this: &Object, _sel: Sel, _outline: id, item: id) -> i8 { + unsafe { + if item == nil { + return 1; + } + + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return 0; + } + + let callbacks = &*(ptr as *const OutlineCallbacks); + let children = children_array(callbacks.roots, item); + let count: u64 = msg_send![children, count]; + (count > 0) as i8 + } +} + +extern "C" fn child_of_item(this: &Object, _sel: Sel, _outline: id, index: i64, item: id) -> id { + unsafe { + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() || index < 0 { + return nil; + } + + let callbacks = &*(ptr as *const OutlineCallbacks); + let children = children_array(callbacks.roots, item); + let count: u64 = msg_send![children, count]; + if (index as u64) >= count { + return nil; + } + + msg_send![children, objectAtIndex: index as u64] + } +} + +extern "C" fn object_value_for_item( + this: &Object, + _sel: Sel, + _outline: id, + _column: id, + item: id, +) -> id { + unsafe { + use super::super::ns_string; + + if item == nil { + return ns_string(""); + } + + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return ns_string(""); + } + + msg_send![item, objectForKey: ns_string("title")] + } +} + +extern "C" fn selection_did_change(this: &Object, _sel: Sel, notification: id) { + unsafe { + use super::super::ns_string; + + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return; + } + + let callbacks = &*(ptr as *const OutlineCallbacks); + if let Some(ref on_select) = callbacks.on_select { + let outline: id = msg_send![notification, object]; + let row: i64 = msg_send![outline, selectedRow]; + if row >= 0 { + let item: id = msg_send![outline, itemAtRow: row]; + if item != nil { + let title_obj: id = msg_send![item, objectForKey: ns_string("title")]; + on_select((row as usize, string_from_ns_string(title_obj))); + } + } + } + } +} + +unsafe fn node_to_dictionary(node: &NativeOutlineNodeData) -> id { + unsafe { + use super::super::ns_string; + + let dict: id = msg_send![class!(NSMutableDictionary), dictionary]; + let _: () = msg_send![dict, setObject: ns_string(&node.title) forKey: ns_string("title")]; + + let children: id = + msg_send![class!(NSMutableArray), arrayWithCapacity: node.children.len() as u64]; + for child in &node.children { + let child_dict = node_to_dictionary(child); + let _: () = msg_send![children, addObject: child_dict]; + } + + let _: () = msg_send![dict, setObject: children forKey: ns_string("children")]; + dict + } +} + +unsafe fn outline_from_scroll(scroll_view: id) -> id { + unsafe { msg_send![scroll_view, documentView] } +} + +pub(crate) unsafe fn create_native_outline_view() -> id { + unsafe { + use super::super::ns_string; + + let outline: id = msg_send![class!(NSOutlineView), alloc]; + let outline: id = msg_send![outline, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(320.0, 220.0), + )]; + let _: () = msg_send![outline, setHeaderView: ptr::null_mut::() as id]; + let _: () = msg_send![outline, setIndentationPerLevel: 14.0f64]; + let _: () = msg_send![outline, setAutoresizingMask: 0u64]; + + let column: id = msg_send![class!(NSTableColumn), alloc]; + let column: id = msg_send![column, initWithIdentifier: ns_string("title")]; + let _: () = msg_send![column, setWidth: 320.0f64]; + let _: () = msg_send![outline, addTableColumn: column]; + let _: () = msg_send![outline, setOutlineTableColumn: column]; + let _: () = msg_send![column, release]; + + let scroll: id = msg_send![class!(NSScrollView), alloc]; + let scroll: id = msg_send![scroll, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(320.0, 220.0), + )]; + let _: () = msg_send![scroll, setHasVerticalScroller: 1i8]; + let _: () = msg_send![scroll, setHasHorizontalScroller: 0i8]; + let _: () = msg_send![scroll, setBorderType: 1u64]; + let _: () = msg_send![scroll, setDocumentView: outline]; + let _: () = msg_send![scroll, setAutoresizingMask: 0u64]; + + scroll + } +} + +pub(crate) unsafe fn set_native_outline_items( + scroll_view: id, + nodes: &[NativeOutlineNodeData], + selected_row: Option, + expand_all: bool, + on_select: Option>, +) -> *mut c_void { + unsafe { + let outline = outline_from_scroll(scroll_view); + + let roots: id = msg_send![class!(NSMutableArray), arrayWithCapacity: nodes.len() as u64]; + for node in nodes { + let dict = node_to_dictionary(node); + let _: () = msg_send![roots, addObject: dict]; + } + let roots: id = msg_send![roots, retain]; + + let callbacks = OutlineCallbacks { roots, on_select }; + + let delegate: id = msg_send![OUTLINE_DELEGATE_CLASS, alloc]; + let delegate: id = msg_send![delegate, init]; + + let callbacks_ptr = Box::into_raw(Box::new(callbacks)) as *mut c_void; + (*delegate).set_ivar::<*mut c_void>(CALLBACK_IVAR, callbacks_ptr); + + let _: () = msg_send![outline, setDataSource: delegate]; + let _: () = msg_send![outline, setDelegate: delegate]; + let _: () = msg_send![outline, reloadData]; + + if expand_all { + let _: () = msg_send![outline, expandItem: nil expandChildren: 1i8]; + } + + if let Some(selected) = selected_row { + let row_count: i64 = msg_send![outline, numberOfRows]; + if row_count > 0 { + let clamped = (selected as i64).min(row_count - 1).max(0); + let index_set: id = + msg_send![class!(NSIndexSet), indexSetWithIndex: clamped as u64]; + let _: () = + msg_send![outline, selectRowIndexes: index_set byExtendingSelection: 0i8]; + } + } + + delegate as *mut c_void + } +} + +pub(crate) unsafe fn set_native_outline_row_height(scroll_view: id, row_height: f64) { + unsafe { + let outline = outline_from_scroll(scroll_view); + let _: () = msg_send![outline, setRowHeight: row_height.max(16.0)]; + } +} + +pub(crate) unsafe fn release_native_outline_target(target: *mut c_void) { + unsafe { + if target.is_null() { + return; + } + + let delegate = target as id; + let callbacks_ptr: *mut c_void = *(*delegate).get_ivar(CALLBACK_IVAR); + if !callbacks_ptr.is_null() { + let _ = Box::from_raw(callbacks_ptr as *mut OutlineCallbacks); + } + let _: () = msg_send![delegate, release]; + } +} + +pub(crate) unsafe fn release_native_outline_view(scroll_view: id) { + unsafe { + let outline = outline_from_scroll(scroll_view); + let _: () = msg_send![outline, setDataSource: ptr::null_mut::() as id]; + let _: () = msg_send![outline, setDelegate: ptr::null_mut::() as id]; + let _: () = msg_send![scroll_view, release]; + } +} diff --git a/crates/gpui/src/platform/mac/native_controls/tab_view.rs b/crates/gpui/src/platform/mac/native_controls/tab_view.rs new file mode 100644 index 0000000..d585d62 --- /dev/null +++ b/crates/gpui/src/platform/mac/native_controls/tab_view.rs @@ -0,0 +1,155 @@ +use super::CALLBACK_IVAR; +use cocoa::{ + base::id, + foundation::{NSPoint, NSRect, NSSize}, +}; +use ctor::ctor; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use std::{ffi::c_void, ptr}; + +struct TabViewCallbacks { + on_select: Option>, +} + +static mut TAB_VIEW_DELEGATE_CLASS: *const Class = ptr::null(); + +#[ctor] +unsafe fn build_tab_view_delegate_class() { + unsafe { + let mut decl = ClassDecl::new("GPUINativeTabViewDelegate", class!(NSObject)).unwrap(); + decl.add_ivar::<*mut c_void>(CALLBACK_IVAR); + + decl.add_method( + sel!(tabView:didSelectTabViewItem:), + did_select_tab_item as extern "C" fn(&Object, Sel, id, id), + ); + + TAB_VIEW_DELEGATE_CLASS = decl.register(); + } +} + +extern "C" fn did_select_tab_item(this: &Object, _sel: Sel, tab_view: id, tab_item: id) { + unsafe { + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return; + } + + let callbacks = &*(ptr as *const TabViewCallbacks); + if let Some(ref on_select) = callbacks.on_select { + let index: i64 = msg_send![tab_view, indexOfTabViewItem: tab_item]; + if index >= 0 { + on_select(index as usize); + } + } + } +} + +pub(crate) unsafe fn create_native_tab_view() -> id { + unsafe { + let tab_view: id = msg_send![class!(NSTabView), alloc]; + let tab_view: id = msg_send![tab_view, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(360.0, 220.0), + )]; + let _: () = msg_send![tab_view, setAutoresizingMask: 0u64]; + tab_view + } +} + +pub(crate) unsafe fn set_native_tab_view_items( + tab_view: id, + labels: &[&str], + selected_index: usize, +) { + unsafe { + use super::super::ns_string; + + let count: i64 = msg_send![tab_view, numberOfTabViewItems]; + for _ in 0..count { + let item: id = msg_send![tab_view, tabViewItemAtIndex: 0i64]; + let _: () = msg_send![tab_view, removeTabViewItem: item]; + } + + for label in labels { + let item: id = msg_send![class!(NSTabViewItem), alloc]; + let item: id = msg_send![item, initWithIdentifier: ns_string(label)]; + let _: () = msg_send![item, setLabel: ns_string(label)]; + + let content: id = msg_send![class!(NSView), alloc]; + let content: id = msg_send![content, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(320.0, 180.0), + )]; + let _: () = msg_send![item, setView: content]; + let _: () = msg_send![content, release]; + + let _: () = msg_send![tab_view, addTabViewItem: item]; + let _: () = msg_send![item, release]; + } + + if !labels.is_empty() { + let clamped = selected_index.min(labels.len() - 1); + let _: () = msg_send![tab_view, selectTabViewItemAtIndex: clamped as i64]; + } + } +} + +pub(crate) unsafe fn set_native_tab_view_selected(tab_view: id, index: usize) { + unsafe { + let count: i64 = msg_send![tab_view, numberOfTabViewItems]; + if count <= 0 { + return; + } + + let clamped = (index as i64).min(count - 1).max(0); + let _: () = msg_send![tab_view, selectTabViewItemAtIndex: clamped]; + } +} + +pub(crate) unsafe fn set_native_tab_view_action( + tab_view: id, + on_select: Option>, +) -> *mut c_void { + unsafe { + let delegate: id = msg_send![TAB_VIEW_DELEGATE_CLASS, alloc]; + let delegate: id = msg_send![delegate, init]; + + let callbacks = TabViewCallbacks { on_select }; + let callbacks_ptr = Box::into_raw(Box::new(callbacks)) as *mut c_void; + (*delegate).set_ivar::<*mut c_void>(CALLBACK_IVAR, callbacks_ptr); + + let _: () = msg_send![tab_view, setDelegate: delegate]; + + delegate as *mut c_void + } +} + +pub(crate) unsafe fn release_native_tab_view_target(target: *mut c_void) { + unsafe { + if target.is_null() { + return; + } + + let delegate = target as id; + let callbacks_ptr: *mut c_void = *(*delegate).get_ivar(CALLBACK_IVAR); + if !callbacks_ptr.is_null() { + let _ = Box::from_raw(callbacks_ptr as *mut TabViewCallbacks); + } + + let _: () = msg_send![delegate, release]; + } +} + +pub(crate) unsafe fn release_native_tab_view(tab_view: id) { + unsafe { + let _: () = msg_send![tab_view, setDelegate: ptr::null_mut::() as id]; + let _: () = msg_send![tab_view, release]; + } +} diff --git a/crates/gpui/src/platform/mac/native_controls/table.rs b/crates/gpui/src/platform/mac/native_controls/table.rs new file mode 100644 index 0000000..1ede2b4 --- /dev/null +++ b/crates/gpui/src/platform/mac/native_controls/table.rs @@ -0,0 +1,307 @@ +use super::CALLBACK_IVAR; +use cocoa::{ + base::{id, nil}, + foundation::{NSPoint, NSRect, NSSize}, +}; +use ctor::ctor; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use std::{ffi::c_void, ptr}; + +struct TableCallbacks { + items: Vec, + on_select: Option>, +} + +static mut TABLE_DELEGATE_CLASS: *const Class = ptr::null(); + +#[ctor] +unsafe fn build_table_delegate_class() { + unsafe { + let mut decl = ClassDecl::new("GPUINativeTableDelegate", class!(NSObject)).unwrap(); + decl.add_ivar::<*mut c_void>(CALLBACK_IVAR); + + decl.add_method( + sel!(numberOfRowsInTableView:), + number_of_rows as extern "C" fn(&Object, Sel, id) -> i64, + ); + decl.add_method( + sel!(tableView:objectValueForTableColumn:row:), + object_value_for_row as extern "C" fn(&Object, Sel, id, id, i64) -> id, + ); + decl.add_method( + sel!(tableViewSelectionDidChange:), + selection_did_change as extern "C" fn(&Object, Sel, id), + ); + + TABLE_DELEGATE_CLASS = decl.register(); + } +} + +extern "C" fn number_of_rows(this: &Object, _sel: Sel, _table: id) -> i64 { + unsafe { + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return 0; + } + let callbacks = &*(ptr as *const TableCallbacks); + callbacks.items.len() as i64 + } +} + +extern "C" fn object_value_for_row( + this: &Object, + _sel: Sel, + _table: id, + _column: id, + row: i64, +) -> id { + unsafe { + use super::super::ns_string; + + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return ns_string(""); + } + let callbacks = &*(ptr as *const TableCallbacks); + if row < 0 || (row as usize) >= callbacks.items.len() { + return ns_string(""); + } + + ns_string(&callbacks.items[row as usize]) + } +} + +extern "C" fn selection_did_change(this: &Object, _sel: Sel, notification: id) { + unsafe { + let ptr: *mut c_void = *this.get_ivar(CALLBACK_IVAR); + if ptr.is_null() { + return; + } + let callbacks = &*(ptr as *const TableCallbacks); + if let Some(ref on_select) = callbacks.on_select { + let table: id = msg_send![notification, object]; + let row: i64 = msg_send![table, selectedRow]; + if row >= 0 { + on_select(row as usize); + } + } + } +} + +unsafe fn table_from_scroll(scroll_view: id) -> id { + unsafe { msg_send![scroll_view, documentView] } +} + +unsafe fn primary_table_column(table: id) -> id { + unsafe { + let columns: id = msg_send![table, tableColumns]; + let count: u64 = msg_send![columns, count]; + if count == 0 { + nil + } else { + msg_send![columns, objectAtIndex: 0u64] + } + } +} + +pub(crate) unsafe fn create_native_table_view() -> id { + unsafe { + use super::super::ns_string; + + let table: id = msg_send![class!(NSTableView), alloc]; + let table: id = msg_send![table, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(320.0, 220.0), + )]; + let _: () = msg_send![table, setUsesAlternatingRowBackgroundColors: 1i8]; + let _: () = msg_send![table, setAllowsMultipleSelection: 0i8]; + let _: () = msg_send![table, setAutoresizingMask: 0u64]; + + let column: id = msg_send![class!(NSTableColumn), alloc]; + let column: id = msg_send![column, initWithIdentifier: ns_string("value")]; + let _: () = msg_send![column, setWidth: 320.0f64]; + let _: () = msg_send![column, setEditable: 0i8]; + let _: () = msg_send![table, addTableColumn: column]; + let _: () = msg_send![column, release]; + + let scroll: id = msg_send![class!(NSScrollView), alloc]; + let scroll: id = msg_send![scroll, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(320.0, 220.0), + )]; + let _: () = msg_send![scroll, setHasVerticalScroller: 1i8]; + let _: () = msg_send![scroll, setHasHorizontalScroller: 0i8]; + let _: () = msg_send![scroll, setBorderType: 1u64]; + let _: () = msg_send![scroll, setDocumentView: table]; + let _: () = msg_send![scroll, setAutoresizingMask: 0u64]; + + scroll + } +} + +pub(crate) unsafe fn set_native_table_column_title(scroll_view: id, title: &str) { + unsafe { + use super::super::ns_string; + let table = table_from_scroll(scroll_view); + let column = primary_table_column(table); + if column != nil { + let header_cell: id = msg_send![column, headerCell]; + if header_cell != nil { + let _: () = msg_send![header_cell, setStringValue: ns_string(title)]; + } + } + } +} + +pub(crate) unsafe fn set_native_table_column_width(scroll_view: id, width: f64) { + unsafe { + let table = table_from_scroll(scroll_view); + let column = primary_table_column(table); + if column != nil { + let _: () = msg_send![column, setWidth: width.max(80.0)]; + } + } +} + +pub(crate) unsafe fn set_native_table_items( + scroll_view: id, + items: &[&str], + selected_index: Option, + on_select: Option>, +) -> *mut c_void { + unsafe { + let table = table_from_scroll(scroll_view); + + let delegate: id = msg_send![TABLE_DELEGATE_CLASS, alloc]; + let delegate: id = msg_send![delegate, init]; + + let callbacks = TableCallbacks { + items: items.iter().map(|item| item.to_string()).collect(), + on_select, + }; + let callbacks_ptr = Box::into_raw(Box::new(callbacks)) as *mut c_void; + (*delegate).set_ivar::<*mut c_void>(CALLBACK_IVAR, callbacks_ptr); + + let _: () = msg_send![table, setDataSource: delegate]; + let _: () = msg_send![table, setDelegate: delegate]; + let _: () = msg_send![table, reloadData]; + + if let Some(index) = selected_index { + let row_count: i64 = msg_send![table, numberOfRows]; + if row_count > 0 { + let clamped = (index as i64).min(row_count - 1).max(0); + let index_set: id = + msg_send![class!(NSIndexSet), indexSetWithIndex: clamped as u64]; + let _: () = msg_send![table, selectRowIndexes: index_set byExtendingSelection: 0i8]; + } + } + + delegate as *mut c_void + } +} + +pub(crate) unsafe fn set_native_table_row_height(scroll_view: id, row_height: f64) { + unsafe { + let table = table_from_scroll(scroll_view); + let _: () = msg_send![table, setRowHeight: row_height.max(16.0)]; + } +} + +pub(crate) unsafe fn set_native_table_row_size_style(scroll_view: id, row_size_style: i64) { + unsafe { + let table = table_from_scroll(scroll_view); + let _: () = msg_send![table, setRowSizeStyle: row_size_style]; + } +} + +pub(crate) unsafe fn set_native_table_style(scroll_view: id, style: i64) { + unsafe { + let table = table_from_scroll(scroll_view); + let _: () = msg_send![table, setStyle: style]; + } +} + +pub(crate) unsafe fn set_native_table_selection_highlight_style( + scroll_view: id, + highlight_style: i64, +) { + unsafe { + let table = table_from_scroll(scroll_view); + let _: () = msg_send![table, setSelectionHighlightStyle: highlight_style]; + } +} + +pub(crate) unsafe fn set_native_table_grid_style(scroll_view: id, grid_style_mask: u64) { + unsafe { + let table = table_from_scroll(scroll_view); + let _: () = msg_send![table, setGridStyleMask: grid_style_mask]; + } +} + +pub(crate) unsafe fn set_native_table_uses_alternating_rows(scroll_view: id, uses: bool) { + unsafe { + let table = table_from_scroll(scroll_view); + let _: () = msg_send![table, setUsesAlternatingRowBackgroundColors: uses as i8]; + } +} + +pub(crate) unsafe fn set_native_table_allows_multiple_selection( + scroll_view: id, + allows_multiple: bool, +) { + unsafe { + let table = table_from_scroll(scroll_view); + let _: () = msg_send![table, setAllowsMultipleSelection: allows_multiple as i8]; + } +} + +pub(crate) unsafe fn set_native_table_show_header(scroll_view: id, show_header: bool) { + unsafe { + let table = table_from_scroll(scroll_view); + if show_header { + let current: id = msg_send![table, headerView]; + if current == nil { + let frame: NSRect = msg_send![table, frame]; + let header: id = msg_send![class!(NSTableHeaderView), alloc]; + let header: id = msg_send![header, initWithFrame: NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(frame.size.width, 17.0), + )]; + let _: () = msg_send![table, setHeaderView: header]; + let _: () = msg_send![header, release]; + } + } else { + let _: () = msg_send![table, setHeaderView: nil]; + } + } +} + +pub(crate) unsafe fn release_native_table_target(target: *mut c_void) { + unsafe { + if target.is_null() { + return; + } + + let delegate = target as id; + let callbacks_ptr: *mut c_void = *(*delegate).get_ivar(CALLBACK_IVAR); + if !callbacks_ptr.is_null() { + let _ = Box::from_raw(callbacks_ptr as *mut TableCallbacks); + } + let _: () = msg_send![delegate, release]; + } +} + +pub(crate) unsafe fn release_native_table_view(scroll_view: id) { + unsafe { + let table = table_from_scroll(scroll_view); + let _: () = msg_send![table, setDataSource: ptr::null_mut::() as id]; + let _: () = msg_send![table, setDelegate: ptr::null_mut::() as id]; + let _: () = msg_send![scroll_view, release]; + } +}