runmat-runtime 0.6.0

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
use runmat_builtins::Value;

use super::axis_ticks::TickAxis;
use super::properties::{resolve_plot_handle, PlotHandle};
use super::state::{
    axis_tick_formats_snapshot, axis_tick_formats_snapshot_for_axes, decode_axes_handle,
    set_axis_tick_formats, set_axis_tick_formats_for_axes,
};
use super::{plotting_error, plotting_error_with_source};
use crate::builtins::plotting::style::value_as_string;
use crate::BuiltinResult;

const DEFAULT_TICK_FORMAT: &str = "%g";

#[derive(Clone, Debug)]
enum TickFormatTarget {
    Current,
    Axes(Vec<(super::state::FigureHandle, usize)>),
}

pub fn axis_tick_format_builtin(
    builtin: &'static str,
    axis: TickAxis,
    args: Vec<Value>,
) -> BuiltinResult<Value> {
    let (target, args) = split_optional_axes_target(builtin, args)?;
    match args.as_slice() {
        [] => query_format(builtin, axis, target),
        [value] => {
            let format = format_from_value(value, builtin)?;
            set_format(builtin, axis, target, format.clone())?;
            Ok(Value::String(format))
        }
        _ => Err(plotting_error(
            builtin,
            format!("{builtin}: expected zero or one tick-format argument"),
        )),
    }
}

fn split_optional_axes_target(
    builtin: &'static str,
    args: Vec<Value>,
) -> BuiltinResult<(TickFormatTarget, Vec<Value>)> {
    let mut iter = args.into_iter();
    let Some(first) = iter.next() else {
        return Ok((TickFormatTarget::Current, Vec::new()));
    };
    if let Ok(PlotHandle::Axes(handle, axes_index)) = resolve_plot_handle(&first, builtin) {
        return Ok((
            TickFormatTarget::Axes(vec![(handle, axes_index)]),
            iter.collect(),
        ));
    }
    if let Some(targets) = axes_array_targets(&first) {
        return Ok((TickFormatTarget::Axes(targets), iter.collect()));
    }
    let mut rest = Vec::with_capacity(iter.size_hint().0 + 1);
    rest.push(first);
    rest.extend(iter);
    Ok((TickFormatTarget::Current, rest))
}

fn axes_array_targets(value: &Value) -> Option<Vec<(super::state::FigureHandle, usize)>> {
    let Value::Tensor(tensor) = value else {
        return None;
    };
    if tensor.data.is_empty() {
        return None;
    }
    let mut targets = Vec::with_capacity(tensor.data.len());
    for scalar in &tensor.data {
        let Ok((handle, axes_index)) = decode_axes_handle(*scalar) else {
            return None;
        };
        if !super::state::axes_handle_exists(handle, axes_index) {
            return None;
        }
        targets.push((handle, axes_index));
    }
    Some(targets)
}

fn query_format(
    builtin: &'static str,
    axis: TickAxis,
    target: TickFormatTarget,
) -> BuiltinResult<Value> {
    let (x_format, y_format) = match target {
        TickFormatTarget::Axes(targets) if targets.len() == 1 => {
            let (handle, axes_index) = targets[0];
            axis_tick_formats_snapshot_for_axes(handle, axes_index).map_err(|err| {
                plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
            })?
        }
        TickFormatTarget::Axes(_) => {
            return Err(plotting_error(
                builtin,
                format!("{builtin}: query form requires a scalar axes handle"),
            ))
        }
        TickFormatTarget::Current => axis_tick_formats_snapshot(),
    };
    let format = match axis {
        TickAxis::X => x_format,
        TickAxis::Y => y_format,
    };
    Ok(Value::String(
        format.unwrap_or_else(|| DEFAULT_TICK_FORMAT.into()),
    ))
}

fn set_format(
    builtin: &'static str,
    axis: TickAxis,
    target: TickFormatTarget,
    format: String,
) -> BuiltinResult<()> {
    match target {
        TickFormatTarget::Axes(targets) => {
            for (handle, axes_index) in targets {
                let (x_format, y_format) = axis_tick_formats_snapshot_for_axes(handle, axes_index)
                    .map_err(|err| {
                        plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
                    })?;
                let (x, y) = match axis {
                    TickAxis::X => (Some(format.clone()), y_format),
                    TickAxis::Y => (x_format, Some(format.clone())),
                };
                set_axis_tick_formats_for_axes(handle, axes_index, x, y).map_err(|err| {
                    plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
                })?;
            }
            Ok(())
        }
        TickFormatTarget::Current => {
            let (x_format, y_format) = axis_tick_formats_snapshot();
            let (x, y) = match axis {
                TickAxis::X => (Some(format), y_format),
                TickAxis::Y => (x_format, Some(format)),
            };
            set_axis_tick_formats(x, y);
            Ok(())
        }
    }
}

fn format_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<String> {
    let format = value_as_string(value).ok_or_else(|| {
        plotting_error(
            builtin,
            format!("{builtin}: tick format must be a string scalar"),
        )
    })?;
    Ok(runmat_plot::core::plot_renderer::plot_utils::canonical_tick_label_format(&format))
}