mkapk-test-host 0.1.1

Cross-platform audio plugin build and packaging system (Standalone/VST3/AU/AAX) with a shared retained-mode GUI framework
#![allow(unexpected_cfgs, deprecated)]

use std::cell::Cell;
use std::ptr::null_mut;

use mkapk_core::{
    CommandList, LayoutConstraints, LayoutDirection, LayoutEngine, LayoutNode, LayoutResult,
    PaintCommand, Rectf, Sizef, TraverseOrder, Tree, Widget, WidgetId, downcast_widget_ref,
};
use mkapk_host::{
    EditorHost, NormalizedValue, ParameterId, ParentWindowHandle, PluginEditor, SizeConstraints,
};
use mkapk_widgets::{Button, Knob, Label, Slider, Theme};

#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;

struct Panel {
    id: WidgetId,
    theme: Theme,
    frame: Cell<Rectf>,
}

impl Panel {
    fn new(theme: Theme) -> Self {
        Self {
            id: WidgetId::new(),
            theme,
            frame: Cell::new(Rectf::default()),
        }
    }

    fn set_frame(&self, frame: Rectf) {
        self.frame.set(frame);
    }
}

impl Widget for Panel {
    fn id(&self) -> WidgetId {
        self.id
    }

    fn layout(&self, _constraints: LayoutConstraints) -> Sizef {
        Sizef::zero()
    }

    fn paint(&self, commands: &mut CommandList) {
        commands.push(PaintCommand::FillRect {
            rect: self.frame.get(),
            color: self.theme.background,
        });
    }
}

struct ControlsEditor {
    view: *mut core::ffi::c_void,
    size: Sizef,
    scale: f32,
    tree: Tree,
    layout_engine: LayoutEngine,
    layout: LayoutResult,
    _label: WidgetId,
    _slider: WidgetId,
    _knob: WidgetId,
    _button: WidgetId,
}

impl ControlsEditor {
    fn new() -> Self {
        let theme = Theme::default();
        let mut tree = Tree::new();

        let panel = Panel::new(theme);
        let root = panel.id();
        tree.insert(Box::new(panel), None);

        let label = Label::new("Controls Demo", theme);
        let label_id = label.id();
        tree.insert(Box::new(label), Some(root));

        let slider = Slider::new(ParameterId(1), NormalizedValue::new(0.5), theme);
        let slider_id = slider.id();
        tree.insert(Box::new(slider), Some(root));

        let knob = Knob::new(ParameterId(2), NormalizedValue::new(0.25), theme);
        let knob_id = knob.id();
        tree.insert(Box::new(knob), Some(root));

        let button = Button::new("Click Me", theme);
        let button_id = button.id();
        tree.insert(Box::new(button), Some(root));

        let mut layout_engine = LayoutEngine::new();
        layout_engine.set_node(LayoutNode {
            id: root,
            direction: LayoutDirection::Column,
            padding: theme.padding,
            ..LayoutNode::default()
        });

        let margin = mkapk_core::Insetsf::uniform(4.0);
        for &id in &[label_id, slider_id, knob_id, button_id] {
            layout_engine.set_node(LayoutNode {
                id,
                margin,
                ..LayoutNode::default()
            });
        }

        let size = Sizef::new(400.0, 300.0);
        let constraints = LayoutConstraints {
            min_width: Some(size.width),
            max_width: Some(size.width),
            min_height: Some(size.height),
            max_height: Some(size.height),
        };

        let layout = layout_engine.compute(&tree, constraints);

        let mut editor = Self {
            view: null_mut(),
            size,
            scale: 1.0,
            tree,
            layout_engine,
            layout,
            _label: label_id,
            _slider: slider_id,
            _knob: knob_id,
            _button: button_id,
        };
        editor.apply_layout();
        editor
    }

    fn apply_layout(&mut self) {
        for (&id, layout_box) in self.layout.iter() {
            let Some(node) = self.tree.find(id) else {
                continue;
            };
            let widget = node.widget.borrow();
            let frame = Rectf::new(layout_box.origin, layout_box.size);
            if let Some(panel) = downcast_widget_ref::<Panel>(&**widget) {
                panel.set_frame(frame);
            } else if let Some(label) = downcast_widget_ref::<Label>(&**widget) {
                label.set_frame(frame);
            } else if let Some(slider) = downcast_widget_ref::<Slider>(&**widget) {
                slider.set_frame(frame);
            } else if let Some(knob) = downcast_widget_ref::<Knob>(&**widget) {
                knob.set_frame(frame);
            } else if let Some(button) = downcast_widget_ref::<Button>(&**widget) {
                button.set_frame(frame);
            }
        }
    }

    fn rebuild_commands(&self) -> CommandList {
        let mut commands = CommandList::new();
        for id in self.tree.traverse(TraverseOrder::PreOrder) {
            if let Some(node) = self.tree.find(id) {
                node.widget.borrow().paint(&mut commands);
            }
        }
        commands
    }
}

#[cfg(target_os = "macos")]
fn backing_scale_for_view(view: *mut core::ffi::c_void) -> f32 {
    use objc::runtime::{BOOL, Object, YES};

    if view.is_null() {
        return 1.0;
    }
    unsafe {
        let view = view as *mut Object;
        let can_draw: BOOL = msg_send![view, canDraw];
        if can_draw != YES {
            return 1.0;
        }
        let window: *mut Object = msg_send![view, window];
        if window.is_null() {
            return 1.0;
        }
        let scale: f64 = msg_send![window, backingScaleFactor];
        scale as f32
    }
}

#[cfg(not(target_os = "macos"))]
fn backing_scale_for_view(_view: *mut core::ffi::c_void) -> f32 {
    1.0
}

impl PluginEditor for ControlsEditor {
    fn open(&mut self, parent: ParentWindowHandle, _host: &dyn EditorHost) {
        if let ParentWindowHandle::Mac(ptr) = parent {
            self.view = ptr;
            self.scale = backing_scale_for_view(ptr);
        }
    }

    fn close(&mut self) {}

    fn resize(&mut self, size: Sizef) {
        self.size = size;
        let constraints = LayoutConstraints {
            min_width: Some(size.width),
            max_width: Some(size.width),
            min_height: Some(size.height),
            max_height: Some(size.height),
        };
        self.layout = self.layout_engine.compute(&self.tree, constraints);
        self.apply_layout();
    }

    fn idle(&mut self) {
        let commands = self.rebuild_commands();
        mkapk_mac::render_to_view(self.view, self.size, self.scale, &commands);
    }

    fn on_parameter_changed(&mut self, _id: ParameterId, _value: NormalizedValue) {}

    fn size_constraints(&self) -> SizeConstraints {
        SizeConstraints::default()
    }
}

fn main() {
    let mut duration_ms = 1000;
    let mut width = 400;
    let mut height = 300;

    let mut args = std::env::args().skip(1);
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--duration-ms" => {
                duration_ms = args
                    .next()
                    .expect("--duration-ms requires a value")
                    .parse()
                    .expect("duration must be a number");
            }
            "--width" => {
                width = args
                    .next()
                    .expect("--width requires a value")
                    .parse()
                    .expect("width must be a number");
            }
            "--height" => {
                height = args
                    .next()
                    .expect("--height requires a value")
                    .parse()
                    .expect("height must be a number");
            }
            other => eprintln!("warning: unknown argument {other}"),
        }
    }

    mkapk_test_host::run_test_host_with_editor(duration_ms, width, height, ControlsEditor::new());
}