use crate::error::Result;
#[cfg(not(test))]
use crate::view::{Orientation, View, ViewKind};
#[cfg(test)]
use crate::view::View;
#[cfg(not(test))]
use cocoa::foundation::{NSPoint, NSRect, NSSize};
#[cfg(not(test))]
use objc::runtime::{Class, Object};
#[cfg(not(test))]
use objc::{msg_send, sel, sel_impl};
#[cfg(not(test))]
pub fn render(root: &View, content_view: *mut Object, bounds: NSRect) -> Result<()> {
render_view(root, content_view, bounds)
}
#[cfg(not(test))]
fn render_view(view: &View, parent: *mut Object, bounds: NSRect) -> Result<()> {
if view.style.hidden {
return Ok(());
}
match &view.kind {
ViewKind::VStack { spacing } => render_vstack(view, parent, bounds, *spacing),
ViewKind::HStack { spacing } => render_hstack(view, parent, bounds, *spacing),
ViewKind::ZStack => render_zstack(view, parent, bounds),
ViewKind::Spacer { .. } => Ok(()), ViewKind::Text(s) => render_label(s, false, parent, bounds, &view.style),
ViewKind::Label(s) => render_label(s, false, parent, bounds, &view.style),
ViewKind::Button { title } => {
render_button(title, parent, bounds, &view.style, &view.event)
}
ViewKind::TextField { placeholder, value } => {
render_text_field(placeholder, value, true, parent, bounds)
}
ViewKind::SecureField { placeholder } => render_secure_field(placeholder, parent, bounds),
ViewKind::Checkbox { title, checked } => {
render_check_or_radio(title, *checked, 3, parent, bounds, &view.event)
}
ViewKind::Radio { title, selected } => {
render_check_or_radio(title, *selected, 4, parent, bounds, &view.event)
}
ViewKind::Slider { min, max, value } => {
render_slider(*min, *max, *value, parent, bounds, &view.event)
}
ViewKind::Toggle { title, on } => {
render_check_or_radio(title, *on, 3, parent, bounds, &view.event)
}
ViewKind::Dropdown {
title: _,
options,
selected,
} => render_dropdown(options, *selected, parent, bounds, &view.event),
ViewKind::ScrollView { .. } => render_scroll_view(view, parent, bounds),
ViewKind::TabView { tabs, selected } => {
render_tab_view(tabs, *selected, view, parent, bounds)
}
ViewKind::SplitView { orientation, ratio } => {
render_split_view(*orientation, *ratio, view, parent, bounds)
}
ViewKind::GroupBox { title } => render_group_box(title, view, parent, bounds),
ViewKind::ProgressBar { value, max } => render_progress(*value, *max, parent, bounds),
ViewKind::Image { name } => render_image(name, parent, bounds),
ViewKind::TextArea { placeholder, value } => {
render_text_area(placeholder, value, parent, bounds, &view.event)
}
ViewKind::DatePicker { .. } => render_date_picker(parent, bounds, &view.event),
ViewKind::ColorPicker { .. } => render_color_picker(parent, bounds, &view.event),
ViewKind::WebView { url } => render_web_view(url, parent, bounds),
ViewKind::TableView { columns, rows } => render_table_view(columns, rows, parent, bounds),
ViewKind::SegmentedControl { segments, selected } => {
render_segmented_control(segments, *selected, parent, bounds, &view.event)
}
ViewKind::ComboBox { items, value } => {
render_combo_box(items, value, parent, bounds, &view.event)
}
ViewKind::SearchField { placeholder, value } => {
render_search_field(placeholder, value, parent, bounds, &view.event)
}
ViewKind::Stepper {
value,
min,
max,
step,
} => render_stepper(*value, *min, *max, *step, parent, bounds, &view.event),
ViewKind::LevelIndicator {
value,
min,
max,
style,
} => render_level_indicator(*value, *min, *max, *style, parent, bounds),
ViewKind::PathControl { path } => render_path_control(path, parent, bounds),
ViewKind::Custom { .. } => Ok(()), }
}
#[cfg(not(test))]
fn ns_string(s: &str) -> Result<*mut Object> {
let cstr = std::ffi::CString::new(s)?;
unsafe {
let cls = objc::class!(NSString);
let obj: *mut Object = msg_send![cls, stringWithUTF8String: cstr.as_ptr()];
Ok(obj)
}
}
#[cfg(not(test))]
fn rect(x: f64, y: f64, w: f64, h: f64) -> NSRect {
NSRect {
origin: NSPoint { x, y },
size: NSSize {
width: w,
height: h,
},
}
}
#[cfg(not(test))]
fn wire_event(control: *mut Object, event: &Option<crate::view::EventBinding>) {
if let Some(eb) = event {
let handler = crate::event::action_handler();
let tag = eb.callback_id as isize;
crate::event::map_tag(tag, eb.callback_id);
unsafe {
let _: () = msg_send![control, setTag: tag];
let _: () = msg_send![control, setTarget: handler];
let _: () = msg_send![control, setAction: objc::sel!(handleAction:)];
}
}
}
#[cfg(not(test))]
fn default_height(view: &View) -> f64 {
if let Some(h) = view.style.height {
return h;
}
match &view.kind {
ViewKind::Spacer { .. } => 0.0, ViewKind::Text(_) | ViewKind::Label(_) => {
let font_size = view.style.font_size.unwrap_or(13.0);
(font_size * 1.5).max(24.0)
}
ViewKind::Button { .. } => 32.0,
ViewKind::TextField { .. } | ViewKind::SecureField { .. } => 28.0,
ViewKind::Checkbox { .. } | ViewKind::Radio { .. } | ViewKind::Toggle { .. } => 24.0,
ViewKind::Slider { .. } => 24.0,
ViewKind::Dropdown { .. } => 28.0,
ViewKind::ProgressBar { .. } => 20.0,
ViewKind::TextArea { .. } => 80.0,
ViewKind::DatePicker { .. } => 28.0,
ViewKind::ColorPicker { .. } => 44.0,
ViewKind::WebView { .. } => 200.0,
ViewKind::TableView { .. } => 150.0,
ViewKind::SegmentedControl { .. } => 28.0,
ViewKind::ComboBox { .. } => 28.0,
ViewKind::SearchField { .. } => 28.0,
ViewKind::Stepper { .. } => 28.0,
ViewKind::LevelIndicator { .. } => 20.0,
ViewKind::PathControl { .. } => 28.0,
ViewKind::HStack { .. } => 36.0,
ViewKind::VStack { .. } => 200.0,
_ => 40.0,
}
}
#[cfg(not(test))]
fn render_vstack(view: &View, parent: *mut Object, bounds: NSRect, spacing: f64) -> Result<()> {
let visible: Vec<&View> = view.children.iter().filter(|c| !c.style.hidden).collect();
if visible.is_empty() {
return Ok(());
}
let spacer_count = visible
.iter()
.filter(|c| matches!(c.kind, ViewKind::Spacer { .. }))
.count();
let gap_count = (visible.len() as f64 - 1.0).max(0.0);
let total_spacing = spacing * gap_count;
let fixed_height: f64 = visible
.iter()
.filter(|c| !matches!(c.kind, ViewKind::Spacer { .. }))
.map(|c| default_height(c))
.sum();
let remaining = (bounds.size.height - fixed_height - total_spacing).max(0.0);
let spacer_h = if spacer_count > 0 {
remaining / spacer_count as f64
} else {
0.0
};
let mut y = bounds.origin.y + bounds.size.height;
for child in &visible {
let h = if matches!(child.kind, ViewKind::Spacer { .. }) {
spacer_h
} else {
default_height(child)
};
y -= h;
let child_bounds = rect(bounds.origin.x, y, bounds.size.width, h);
render_view(child, parent, child_bounds)?;
y -= spacing;
}
Ok(())
}
#[cfg(not(test))]
fn render_hstack(view: &View, parent: *mut Object, bounds: NSRect, spacing: f64) -> Result<()> {
let visible: Vec<&View> = view.children.iter().filter(|c| !c.style.hidden).collect();
if visible.is_empty() {
return Ok(());
}
let gap_count = (visible.len() as f64 - 1.0).max(0.0);
let total_spacing = spacing * gap_count;
let child_w = (bounds.size.width - total_spacing) / visible.len() as f64;
let mut x = bounds.origin.x;
for child in &visible {
let w = child.style.width.unwrap_or(child_w);
let child_bounds = rect(x, bounds.origin.y, w, bounds.size.height);
render_view(child, parent, child_bounds)?;
x += w + spacing;
}
Ok(())
}
#[cfg(not(test))]
fn render_zstack(view: &View, parent: *mut Object, bounds: NSRect) -> Result<()> {
for child in &view.children {
render_view(child, parent, bounds)?;
}
Ok(())
}
#[cfg(not(test))]
fn render_label(
text: &str,
_editable: bool,
parent: *mut Object,
bounds: NSRect,
style: &crate::view::ViewStyle,
) -> Result<()> {
unsafe {
let cls = Class::get("NSTextField").ok_or("NSTextField not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let ns = ns_string(text)?;
let _: () = msg_send![view, setStringValue: ns];
let _: () = msg_send![view, setBezeled: false];
let _: () = msg_send![view, setDrawsBackground: false];
let _: () = msg_send![view, setEditable: false];
let _: () = msg_send![view, setSelectable: false];
if style.font_bold {
let font: *mut Object = msg_send![objc::class!(NSFont), boldSystemFontOfSize: style.font_size.unwrap_or(13.0)];
let _: () = msg_send![view, setFont: font];
} else if let Some(size) = style.font_size {
let font: *mut Object = msg_send![objc::class!(NSFont), systemFontOfSize: size];
let _: () = msg_send![view, setFont: font];
}
set_view_tag(view, style);
set_accessibility(view, style);
apply_native_styles(view, parent, style);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_button(
title: &str,
parent: *mut Object,
bounds: NSRect,
_style: &crate::view::ViewStyle,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let cls = Class::get("NSButton").ok_or("NSButton not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let ns = ns_string(title)?;
let _: () = msg_send![view, setTitle: ns];
let _: () = msg_send![view, setButtonType: 0_i64];
let _: () = msg_send![view, setBezelStyle: 4_i64];
let _: () = msg_send![view, setEnabled: true];
wire_event(view, event);
set_accessibility(view, _style);
apply_native_styles(view, parent, _style);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_text_field(
placeholder: &str,
value: &str,
editable: bool,
parent: *mut Object,
bounds: NSRect,
) -> Result<()> {
unsafe {
let cls = Class::get("NSTextField").ok_or("NSTextField not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let ns_val = ns_string(value)?;
let ns_ph = ns_string(placeholder)?;
let _: () = msg_send![view, setStringValue: ns_val];
let _: () = msg_send![view, setPlaceholderString: ns_ph];
let _: () = msg_send![view, setBezeled: true];
let _: () = msg_send![view, setDrawsBackground: true];
let _: () = msg_send![view, setEditable: editable];
let _: () = msg_send![view, setSelectable: true];
let _: () = msg_send![view, setEnabled: true];
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_secure_field(placeholder: &str, parent: *mut Object, bounds: NSRect) -> Result<()> {
unsafe {
let cls = Class::get("NSSecureTextField").ok_or("NSSecureTextField not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let ns_ph = ns_string(placeholder)?;
let _: () = msg_send![view, setPlaceholderString: ns_ph];
let _: () = msg_send![view, setBezeled: true];
let _: () = msg_send![view, setDrawsBackground: true];
let _: () = msg_send![view, setEditable: true];
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_check_or_radio(
title: &str,
checked: bool,
button_type: i64,
parent: *mut Object,
bounds: NSRect,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let cls = Class::get("NSButton").ok_or("NSButton not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let ns = ns_string(title)?;
let _: () = msg_send![view, setTitle: ns];
let _: () = msg_send![view, setButtonType: button_type];
let _: () = msg_send![view, setEnabled: true];
let state: i64 = if checked { 1 } else { 0 };
let _: () = msg_send![view, setState: state];
wire_event(view, event);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_slider(
min: f64,
max: f64,
value: f64,
parent: *mut Object,
bounds: NSRect,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let cls = Class::get("NSSlider").ok_or("NSSlider not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let _: () = msg_send![view, setMinValue: min];
let _: () = msg_send![view, setMaxValue: max];
let _: () = msg_send![view, setDoubleValue: value];
let _: () = msg_send![view, setEnabled: true];
let _: () = msg_send![view, setContinuous: true];
wire_event(view, event);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_dropdown(
options: &[String],
selected: usize,
parent: *mut Object,
bounds: NSRect,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let cls = Class::get("NSPopUpButton").ok_or("NSPopUpButton not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds pullsDown: false];
for opt in options {
let ns = ns_string(opt)?;
let _: () = msg_send![view, addItemWithTitle: ns];
}
if !options.is_empty() {
let _: () = msg_send![view, selectItemAtIndex: selected as i64];
}
wire_event(view, event);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_scroll_view(view: &View, parent: *mut Object, bounds: NSRect) -> Result<()> {
unsafe {
let cls = Class::get("NSScrollView").ok_or("NSScrollView not found")?;
let sv: *mut Object = msg_send![cls, alloc];
let sv: *mut Object = msg_send![sv, initWithFrame: bounds];
let _: () = msg_send![sv, setHasVerticalScroller: true];
let _: () = msg_send![sv, setHasHorizontalScroller: false];
let _: () = msg_send![sv, setAutohidesScrollers: true];
let clip: *mut Object = msg_send![sv, contentView];
let clip_bounds: NSRect = msg_send![clip, bounds];
for child in &view.children {
render_view(child, sv, clip_bounds)?;
}
let _: () = msg_send![parent, addSubview: sv];
}
Ok(())
}
#[cfg(not(test))]
fn render_tab_view(
tabs: &[String],
selected: usize,
view: &View,
parent: *mut Object,
bounds: NSRect,
) -> Result<()> {
unsafe {
let cls = Class::get("NSTabView").ok_or("NSTabView not found")?;
let tv: *mut Object = msg_send![cls, alloc];
let tv: *mut Object = msg_send![tv, initWithFrame: bounds];
for (i, tab_title) in tabs.iter().enumerate() {
let item_cls = Class::get("NSTabViewItem").ok_or("NSTabViewItem not found")?;
let ns_id = ns_string(&i.to_string())?;
let item: *mut Object = msg_send![item_cls, alloc];
let item: *mut Object = msg_send![item, initWithIdentifier: ns_id];
let ns_title = ns_string(tab_title)?;
let _: () = msg_send![item, setLabel: ns_title];
if let Some(child) = view.children.get(i) {
let tab_content: *mut Object = msg_send![item, view];
if !tab_content.is_null() {
let tab_bounds: NSRect = msg_send![tab_content, bounds];
render_view(child, tab_content, tab_bounds)?;
}
}
let _: () = msg_send![tv, addTabViewItem: item];
}
if !tabs.is_empty() {
let _: () = msg_send![tv, selectTabViewItemAtIndex: selected as i64];
}
let _: () = msg_send![parent, addSubview: tv];
}
Ok(())
}
#[cfg(not(test))]
fn render_split_view(
orientation: Orientation,
_ratio: f64,
view: &View,
parent: *mut Object,
bounds: NSRect,
) -> Result<()> {
unsafe {
let cls = Class::get("NSSplitView").ok_or("NSSplitView not found")?;
let sv: *mut Object = msg_send![cls, alloc];
let sv: *mut Object = msg_send![sv, initWithFrame: bounds];
let vertical = matches!(orientation, Orientation::Vertical);
let _: () = msg_send![sv, setVertical: vertical];
for child in &view.children {
let child_view_cls = Class::get("NSView").ok_or("NSView not found")?;
let child_ns: *mut Object = msg_send![child_view_cls, alloc];
let child_ns: *mut Object = msg_send![child_ns, initWithFrame: bounds];
let child_bounds: NSRect = msg_send![child_ns, bounds];
render_view(child, child_ns, child_bounds)?;
let _: () = msg_send![sv, addSubview: child_ns];
}
let _: () = msg_send![parent, addSubview: sv];
}
Ok(())
}
#[cfg(not(test))]
fn render_group_box(title: &str, view: &View, parent: *mut Object, bounds: NSRect) -> Result<()> {
unsafe {
let cls = Class::get("NSBox").ok_or("NSBox not found")?;
let bx: *mut Object = msg_send![cls, alloc];
let bx: *mut Object = msg_send![bx, initWithFrame: bounds];
let ns = ns_string(title)?;
let _: () = msg_send![bx, setTitle: ns];
let _: () = msg_send![bx, setBoxType: 0_i64];
let _: () = msg_send![bx, setBorderType: 2_i64];
let content: *mut Object = msg_send![bx, contentView];
if !content.is_null() {
let content_bounds: NSRect = msg_send![content, bounds];
for child in &view.children {
render_view(child, content, content_bounds)?;
}
}
let _: () = msg_send![parent, addSubview: bx];
}
Ok(())
}
#[cfg(not(test))]
fn render_progress(value: f64, max: f64, parent: *mut Object, bounds: NSRect) -> Result<()> {
unsafe {
let cls = Class::get("NSProgressIndicator").ok_or("NSProgressIndicator not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let _: () = msg_send![view, setIndeterminate: false];
let _: () = msg_send![view, setMinValue: 0.0_f64];
let _: () = msg_send![view, setMaxValue: max];
let _: () = msg_send![view, setDoubleValue: value];
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_image(name: &str, parent: *mut Object, bounds: NSRect) -> Result<()> {
unsafe {
let cls = Class::get("NSImageView").ok_or("NSImageView not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let ns_name = ns_string(name)?;
let image: *mut Object = msg_send![objc::class!(NSImage), imageNamed: ns_name];
if !image.is_null() {
let _: () = msg_send![view, setImage: image];
}
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_text_area(
placeholder: &str,
value: &str,
parent: *mut Object,
bounds: NSRect,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let sv_cls = Class::get("NSScrollView").ok_or("NSScrollView not found")?;
let sv: *mut Object = msg_send![sv_cls, alloc];
let sv: *mut Object = msg_send![sv, initWithFrame: bounds];
let _: () = msg_send![sv, setHasVerticalScroller: true];
let _: () = msg_send![sv, setBorderType: 2_i64];
let tv_cls = Class::get("NSTextView").ok_or("NSTextView not found")?;
let content_size: NSSize = msg_send![sv, contentSize];
let tv_frame = rect(0.0, 0.0, content_size.width, content_size.height);
let tv: *mut Object = msg_send![tv_cls, alloc];
let tv: *mut Object = msg_send![tv, initWithFrame: tv_frame];
let _: () = msg_send![tv, setEditable: true];
let _: () = msg_send![tv, setRichText: false];
let _: () = msg_send![tv, setAutoresizingMask: 1_u64];
if !value.is_empty() {
let ns_val = ns_string(value)?;
let _: () = msg_send![tv, setString: ns_val];
}
if !placeholder.is_empty() {
if value.is_empty() {
let ns_ph = ns_string(placeholder)?;
let _: () = msg_send![tv, setString: ns_ph];
}
}
let _: () = msg_send![sv, setDocumentView: tv];
wire_event(tv, event);
let _: () = msg_send![parent, addSubview: sv];
}
Ok(())
}
#[cfg(not(test))]
fn render_date_picker(
parent: *mut Object,
bounds: NSRect,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let cls = Class::get("NSDatePicker").ok_or("NSDatePicker not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let _: () = msg_send![view, setDatePickerStyle: 0_i64]; let _: () = msg_send![view, setDatePickerElements: 0x000c_i64]; let _: () = msg_send![view, setBezeled: true];
let _: () = msg_send![view, setDrawsBackground: true];
let _: () = msg_send![view, setEnabled: true];
wire_event(view, event);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_color_picker(
parent: *mut Object,
bounds: NSRect,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let cls = Class::get("NSColorWell").ok_or("NSColorWell not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let _: () = msg_send![view, setEnabled: true];
wire_event(view, event);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_web_view(url: &str, parent: *mut Object, bounds: NSRect) -> Result<()> {
unsafe {
let cls = Class::get("WKWebView").ok_or("WKWebView not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let url_ns = ns_string(url)?;
let nsurl_cls = Class::get("NSURL").ok_or("NSURL not found")?;
let nsurl: *mut Object = msg_send![nsurl_cls, URLWithString: url_ns];
if !nsurl.is_null() {
let req_cls = Class::get("NSURLRequest").ok_or("NSURLRequest not found")?;
let req: *mut Object = msg_send![req_cls, requestWithURL: nsurl];
let _: () = msg_send![view, loadRequest: req];
}
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_table_view(
columns: &[String],
rows: &[Vec<String>],
parent: *mut Object,
bounds: NSRect,
) -> Result<()> {
unsafe {
let sv_cls = Class::get("NSScrollView").ok_or("NSScrollView not found")?;
let sv: *mut Object = msg_send![sv_cls, alloc];
let sv: *mut Object = msg_send![sv, initWithFrame: bounds];
let _: () = msg_send![sv, setHasVerticalScroller: true];
let _: () = msg_send![sv, setBorderType: 2_i64];
let tv_cls = Class::get("NSTableView").ok_or("NSTableView not found")?;
let tv: *mut Object = msg_send![tv_cls, alloc];
let tv: *mut Object = msg_send![tv, initWithFrame: bounds];
for col_title in columns {
let col_cls = Class::get("NSTableColumn").ok_or("NSTableColumn not found")?;
let col_id = ns_string(col_title)?;
let col: *mut Object = msg_send![col_cls, alloc];
let col: *mut Object = msg_send![col, initWithIdentifier: col_id];
let header: *mut Object = msg_send![col, headerCell];
let ns_title = ns_string(col_title)?;
let _: () = msg_send![header, setStringValue: ns_title];
let _: () = msg_send![tv, addTableColumn: col];
}
let row_count = rows.len();
if row_count > 0 {
eprintln!(
"[renderer] TableView: {} columns, {} rows (data source not yet wired)",
columns.len(),
row_count
);
}
let _: () = msg_send![sv, setDocumentView: tv];
let _: () = msg_send![parent, addSubview: sv];
}
Ok(())
}
#[cfg(not(test))]
fn render_segmented_control(
segments: &[String],
selected: usize,
parent: *mut Object,
bounds: NSRect,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let cls = Class::get("NSSegmentedControl").ok_or("NSSegmentedControl not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let _: () = msg_send![view, setSegmentCount: segments.len() as i64];
for (i, segment) in segments.iter().enumerate() {
let ns = ns_string(segment)?;
let _: () = msg_send![view, setLabel: ns forSegment: i as i64];
}
if !segments.is_empty() {
let _: () = msg_send![view, setSelectedSegment: selected as i64];
}
wire_event(view, event);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_combo_box(
items: &[String],
value: &str,
parent: *mut Object,
bounds: NSRect,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let cls = Class::get("NSComboBox").ok_or("NSComboBox not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
for item in items {
let ns = ns_string(item)?;
let _: () = msg_send![view, addItemWithObjectValue: ns];
}
let ns_val = ns_string(value)?;
let _: () = msg_send![view, setStringValue: ns_val];
wire_event(view, event);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_search_field(
placeholder: &str,
value: &str,
parent: *mut Object,
bounds: NSRect,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let cls = Class::get("NSSearchField").ok_or("NSSearchField not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let ns_ph = ns_string(placeholder)?;
let _: () = msg_send![view, setPlaceholderString: ns_ph];
let ns_val = ns_string(value)?;
let _: () = msg_send![view, setStringValue: ns_val];
wire_event(view, event);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_stepper(
value: f64,
min: f64,
max: f64,
step: f64,
parent: *mut Object,
bounds: NSRect,
event: &Option<crate::view::EventBinding>,
) -> Result<()> {
unsafe {
let cls = Class::get("NSStepper").ok_or("NSStepper not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let _: () = msg_send![view, setMinValue: min];
let _: () = msg_send![view, setMaxValue: max];
let _: () = msg_send![view, setIncrement: step];
let _: () = msg_send![view, setDoubleValue: value];
wire_event(view, event);
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_level_indicator(
value: f64,
min: f64,
max: f64,
style: crate::view::LevelIndicatorStyle,
parent: *mut Object,
bounds: NSRect,
) -> Result<()> {
unsafe {
let cls = Class::get("NSLevelIndicator").ok_or("NSLevelIndicator not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let _: () = msg_send![view, setMinValue: min];
let _: () = msg_send![view, setMaxValue: max];
let _: () = msg_send![view, setDoubleValue: value];
let _: () = msg_send![view, setLevelIndicatorStyle: style as i64];
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn render_path_control(path: &str, parent: *mut Object, bounds: NSRect) -> Result<()> {
unsafe {
let cls = Class::get("NSPathControl").ok_or("NSPathControl not found")?;
let view: *mut Object = msg_send![cls, alloc];
let view: *mut Object = msg_send![view, initWithFrame: bounds];
let ns_path = ns_string(path)?;
let nsurl_cls = Class::get("NSURL").ok_or("NSURL not found")?;
let nsurl: *mut Object = msg_send![nsurl_cls, fileURLWithPath: ns_path];
if !nsurl.is_null() {
let _: () = msg_send![view, setURL: nsurl];
}
let _: () = msg_send![parent, addSubview: view];
}
Ok(())
}
#[cfg(not(test))]
fn set_accessibility(view: *mut Object, style: &crate::view::ViewStyle) {
if let Some(ref label) = style.accessibility_label
&& let Ok(ns) = ns_string(label)
{
unsafe {
let _: () = msg_send![view, setAccessibilityLabel: ns];
}
}
}
#[cfg(not(test))]
fn set_view_tag(view: *mut Object, style: &crate::view::ViewStyle) {
if let Some(tag) = style.tag {
unsafe {
let _: () = msg_send![view, setTag: tag];
}
}
}
#[cfg(not(test))]
fn apply_native_styles(view: *mut Object, superview: *mut Object, style: &crate::view::ViewStyle) {
use crate::native::view_properties;
if let Some(opacity) = style.opacity {
unsafe {
view_properties::set_alpha(view, opacity);
}
}
if style.hidden {
unsafe {
view_properties::set_hidden(view, true);
}
}
if let Some(radius) = style.corner_radius {
unsafe {
view_properties::set_corner_radius(view, radius);
}
}
if let (Some(radius), Some(opacity), Some(offset_x), Some(offset_y)) = (
style.shadow_radius,
style.shadow_opacity,
style.shadow_offset_x,
style.shadow_offset_y,
) {
unsafe {
view_properties::set_shadow(view, radius, opacity, offset_x, offset_y);
}
}
if style.wants_layer {
unsafe {
let _: () = msg_send![view, setWantsLayer: true];
}
}
if style.clips_to_bounds {
unsafe {
let _: () = msg_send![view, setWantsLayer: true];
let layer: *mut Object = msg_send![view, layer];
if !layer.is_null() {
let _: () = msg_send![layer, setMasksToBounds: true];
}
}
}
if style.uses_auto_layout && !style.constraints.is_empty() {
unsafe {
let _ = crate::layout::apply_constraints(view, superview, &style.constraints);
}
} else if style.uses_auto_layout {
unsafe {
crate::layout::set_uses_auto_layout(view, true);
}
}
}
#[cfg(test)]
pub fn render(
root: &View,
_content_view: *mut std::ffi::c_void,
_bounds: (f64, f64, f64, f64),
) -> Result<()> {
walk_tree(root);
Ok(())
}
#[cfg(test)]
fn walk_tree(view: &View) {
for child in &view.children {
walk_tree(child);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_mock() {
let tree = View::vstack()
.child(View::text("Hello"))
.child(View::button("Click"));
let result = render(&tree, std::ptr::null_mut(), (0.0, 0.0, 600.0, 400.0));
assert!(result.is_ok());
assert_eq!(tree.children.len(), 2);
assert_eq!(tree.view_type(), "VStack");
}
#[test]
fn test_render_mock_complex_tree() {
let tree = View::vstack()
.child(View::hstack().child(View::text("A")).child(View::button("B")))
.child(View::tab_view(vec![
String::from("t1"),
String::from("t2"),
]))
.child(View::table_view(
vec![String::from("c")],
vec![vec![String::from("r")]],
));
let r = render(&tree, std::ptr::null_mut(), (0.0, 0.0, 100.0, 100.0));
assert!(r.is_ok());
}
}