use super::color_space_helpers::ColorSpaceInfo;
use crate::config::Config;
use crate::error::{Error, Result};
use crate::processor::Processor;
use crate::transforms::{
DisplayViewTransform, FixedFunctionTransform, GroupTransform, MatrixTransform, Transform,
};
use crate::types::{FixedFunctionStyle, TransformDirection, ROLE_COLOR_PICKING};
use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
const GAMMA: f32 = 2.0;
const LOGSLOPE: f32 = 0.55;
const BREAKPNT: f32 = 0.18;
const NEGSLOPE: f32 = 0.5;
const INVGAMMA: f32 = 0.5; const INVBREAKPNT: f32 = 0.42426406871192851; const LOGOFFSET: f32 = 0.83386419090511021;
fn linear_to_perceptual(linear: f32) -> f32 {
if linear <= 0.0 {
linear * NEGSLOPE
} else if linear > BREAKPNT {
LOGOFFSET + linear.log10() * LOGSLOPE
} else {
linear.powf(INVGAMMA)
}
}
fn perceptual_to_linear(percept: f32) -> f32 {
if percept <= 0.0 {
percept / NEGSLOPE
} else if percept > INVBREAKPNT {
10.0f32.powf((percept - LOGOFFSET) / LOGSLOPE)
} else {
percept.powf(GAMMA)
}
}
#[derive(Debug)]
struct SliderShared {
edges: Mutex<(f32, f32)>,
perceptually_uniform: AtomicBool,
}
#[derive(Debug, Clone)]
pub struct MixingSlider {
shared: Arc<SliderShared>,
}
impl MixingSlider {
fn new() -> Self {
Self {
shared: Arc::new(SliderShared {
edges: Mutex::new((0.0, 1.0)),
perceptually_uniform: AtomicBool::new(false),
}),
}
}
fn edges(&self) -> (f32, f32) {
self.shared
.edges
.lock()
.map(|e| *e)
.unwrap_or_else(|e| *e.into_inner())
}
fn is_perceptually_uniform(&self) -> bool {
self.shared.perceptually_uniform.load(Ordering::Relaxed)
}
pub fn set_slider_min_edge(&self, v: f32) {
if let Ok(mut e) = self.shared.edges.lock() {
e.0 = v;
}
}
pub fn slider_min_edge(&self) -> f32 {
let (min, max) = self.edges();
if !self.is_perceptually_uniform() {
linear_to_perceptual(min.min(max - 0.01))
} else {
min
}
}
pub fn set_slider_max_edge(&self, v: f32) {
if let Ok(mut e) = self.shared.edges.lock() {
e.1 = v;
}
}
pub fn slider_max_edge(&self) -> f32 {
let (min, max) = self.edges();
if !self.is_perceptually_uniform() {
linear_to_perceptual(max.max(min + 0.01))
} else {
max
}
}
pub fn slider_to_mixing(&self, slider_units: f32) -> f32 {
let min = self.slider_min_edge();
let max = self.slider_max_edge();
let percept = min + slider_units * (max - min);
if !self.is_perceptually_uniform() {
perceptual_to_linear(percept)
} else {
percept
}
}
pub fn mixing_to_slider(&self, mixing_units: f32) -> f32 {
let percept = if !self.is_perceptually_uniform() {
linear_to_perceptual(mixing_units)
} else {
mixing_units
};
let min = self.slider_min_edge();
let max = self.slider_max_edge();
(percept - min) / (max - min)
}
}
impl fmt::Display for MixingSlider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"minEdge: {}, maxEdge: {}",
crate::config::utils::fmt_f32(self.slider_min_edge()),
crate::config::utils::fmt_f32(self.slider_max_edge())
)
}
}
const MIXING_ENCODINGS: [&str; 2] = ["RGB", "HSV"];
#[derive(Debug)]
pub struct MixingColorSpaceManager {
config: Arc<Config>,
slider: MixingSlider,
mixing_spaces: Vec<String>,
selected_mixing_space_idx: usize,
selected_mixing_encoding_idx: usize,
color_picker: Option<ColorSpaceInfo>,
}
impl MixingColorSpaceManager {
pub fn new(config: Arc<Config>) -> Self {
let mut m = Self {
config,
slider: MixingSlider::new(),
mixing_spaces: Vec::new(),
selected_mixing_space_idx: 0,
selected_mixing_encoding_idx: 0,
color_picker: None,
};
m.refresh_impl();
m
}
fn update_slider(&self) {
self.slider
.shared
.perceptually_uniform
.store(self.is_perceptually_uniform(), Ordering::Relaxed);
}
fn refresh_impl(&mut self) {
self.selected_mixing_space_idx = 0;
self.mixing_spaces.clear();
self.color_picker = None;
if self.config.has_role(ROLE_COLOR_PICKING) {
self.color_picker = ColorSpaceInfo::from_single_role(&self.config, ROLE_COLOR_PICKING);
if let Some(cp) = &self.color_picker {
self.mixing_spaces.push(cp.ui_name().to_string());
}
} else {
self.mixing_spaces.push("Rendering Space".to_string());
self.mixing_spaces.push("Display Space".to_string());
}
self.selected_mixing_encoding_idx = 0;
self.update_slider();
}
pub fn refresh(&mut self, config: Arc<Config>) {
self.config = config;
self.refresh_impl();
}
pub fn num_mixing_spaces(&self) -> usize {
self.mixing_spaces.len()
}
pub fn mixing_space_ui_name(&self, idx: usize) -> Result<&str> {
self.mixing_spaces
.get(idx)
.map(|s| s.as_str())
.ok_or_else(|| {
Error::msg(format!(
"Invalid mixing space index {} where size is {}.",
idx,
self.mixing_spaces.len()
))
})
}
pub fn selected_mixing_space_idx(&self) -> usize {
self.selected_mixing_space_idx
}
pub fn set_selected_mixing_space_idx(&mut self, idx: usize) -> Result<()> {
if idx >= self.mixing_spaces.len() {
return Err(Error::msg(format!(
"Invalid idx for the mixing space index {} where size is {}.",
idx,
self.mixing_spaces.len()
)));
}
self.selected_mixing_space_idx = idx;
self.update_slider();
Ok(())
}
pub fn set_selected_mixing_space(&mut self, mixing_space: &str) -> Result<()> {
match self.mixing_spaces.iter().position(|s| s == mixing_space) {
Some(idx) => {
self.selected_mixing_space_idx = idx;
self.update_slider();
Ok(())
}
None => Err(Error::msg(format!(
"Invalid mixing space name: '{mixing_space}'."
))),
}
}
pub fn is_perceptually_uniform(&self) -> bool {
match self.color_picker {
None => self.selected_mixing_space_idx != 0,
Some(_) => true,
}
}
pub fn num_mixing_encodings(&self) -> usize {
MIXING_ENCODINGS.len()
}
pub fn mixing_encoding_name(&self, idx: usize) -> Result<&str> {
MIXING_ENCODINGS.get(idx).copied().ok_or_else(|| {
Error::msg(format!(
"Invalid mixing encoding index {} where size is {}.",
idx,
MIXING_ENCODINGS.len()
))
})
}
pub fn selected_mixing_encoding_idx(&self) -> usize {
self.selected_mixing_encoding_idx
}
pub fn set_selected_mixing_encoding_idx(&mut self, idx: usize) -> Result<()> {
if idx >= MIXING_ENCODINGS.len() {
return Err(Error::msg(format!(
"Invalid idx for the mixing encoding index {} where size is {}.",
idx,
MIXING_ENCODINGS.len()
)));
}
self.selected_mixing_encoding_idx = idx;
Ok(())
}
pub fn set_selected_mixing_encoding(&mut self, mixing_encoding: &str) -> Result<()> {
match MIXING_ENCODINGS.iter().position(|s| *s == mixing_encoding) {
Some(idx) => {
self.selected_mixing_encoding_idx = idx;
Ok(())
}
None => Err(Error::msg(format!(
"Invalid mixing encoding: '{mixing_encoding}'."
))),
}
}
fn processor_without_encoding(
&self,
working_name: &str,
display_name: &str,
view_name: &str,
) -> Result<Processor> {
if let Some(cp) = &self.color_picker {
self.config.get_processor(working_name, cp.name())
} else if self.selected_mixing_space_idx > 0 {
let dt = DisplayViewTransform::new(working_name, display_name, view_name);
self.config.get_processor_for_transform(
&Transform::DisplayView(dt),
TransformDirection::Forward,
)
} else {
self.config.get_processor_for_transform(
&Transform::Matrix(MatrixTransform::default()),
TransformDirection::Forward,
)
}
}
pub fn get_processor(
&self,
working_name: &str,
display_name: &str,
view_name: &str,
direction: TransformDirection,
) -> Result<Processor> {
let mut group = GroupTransform::new();
let processor = self.processor_without_encoding(working_name, display_name, view_name)?;
group.append(processor.create_group_transform());
if self.selected_mixing_encoding_idx == 1 {
group.append(FixedFunctionTransform::new(
FixedFunctionStyle::RgbToHsv,
&[],
));
}
self.config
.get_processor_for_transform(&Transform::Group(group), direction)
}
pub fn slider(&self) -> MixingSlider {
self.slider.clone()
}
pub fn slider_with_edges(&self, min_edge: f32, max_edge: f32) -> MixingSlider {
self.slider.set_slider_min_edge(min_edge);
self.slider.set_slider_max_edge(max_edge);
self.slider.clone()
}
}
impl fmt::Display for MixingColorSpaceManager {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "config: {}", self.config.cache_id())?;
write!(f, ", slider: [{}]", self.slider)?;
if !self.mixing_spaces.is_empty() {
write!(f, ", mixingSpaces: [{}]", self.mixing_spaces.join(", "))?;
}
write!(
f,
", selectedMixingSpaceIdx: {}",
self.selected_mixing_space_idx
)?;
write!(
f,
", selectedMixingEncodingIdx: {}",
self.selected_mixing_encoding_idx
)?;
if self.color_picker.is_some() {
write!(f, ", colorPicking")?;
}
Ok(())
}
}