use crate::core::items::Symbol;
use crate::core::transform::Scale;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ProfileToolButton {
dimension: u8,
}
impl Default for ProfileToolButton {
fn default() -> Self {
Self { dimension: 1 }
}
}
impl ProfileToolButton {
pub fn new() -> Self {
Self::default()
}
pub fn dimension(&self) -> u8 {
self.dimension
}
pub fn set_dimension(&mut self, dimension: u8) -> bool {
if matches!(dimension, 1 | 2) && dimension != self.dimension {
self.dimension = dimension;
true
} else {
false
}
}
pub fn action_label(dimension: u8) -> &'static str {
match dimension {
2 => "2D profile on image stack",
_ => "1D profile on visible image",
}
}
pub fn state_tooltip(dimension: u8) -> &'static str {
match dimension {
2 => "2D profile is computed, one 1D profile for each image in the stack",
_ => "1D profile is computed on visible image",
}
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<u8> {
let mut changed = None;
let title = if self.dimension == 2 { "2D" } else { "1D" };
ui.menu_button(title, |ui| {
for dim in [1u8, 2u8] {
let selected = self.dimension == dim;
let resp = ui
.selectable_label(selected, Self::action_label(dim))
.on_hover_text(Self::state_tooltip(dim));
if resp.clicked() {
if self.set_dimension(dim) {
changed = Some(dim);
}
ui.close();
}
}
})
.response
.on_hover_text(Self::state_tooltip(self.dimension));
changed
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SymbolToolChange {
Symbol(Symbol),
Size(f32),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SymbolToolButton {
symbol: Symbol,
size: f32,
}
impl Default for SymbolToolButton {
fn default() -> Self {
Self {
symbol: Symbol::Circle,
size: Self::DEFAULT_SIZE,
}
}
}
impl SymbolToolButton {
pub const MIN_SIZE: f32 = 1.0;
pub const MAX_SIZE: f32 = 20.0;
pub const DEFAULT_SIZE: f32 = 6.0;
pub fn new() -> Self {
Self::default()
}
pub fn symbol(&self) -> Symbol {
self.symbol
}
pub fn set_symbol(&mut self, symbol: Symbol) {
self.symbol = symbol;
}
pub fn size(&self) -> f32 {
self.size
}
pub fn set_size(&mut self, size: f32) {
self.size = size.clamp(Self::MIN_SIZE, Self::MAX_SIZE);
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<SymbolToolChange> {
let mut change = None;
ui.menu_button(self.symbol.name(), |ui| {
let mut size = self.size;
if ui
.add(egui::Slider::new(&mut size, Self::MIN_SIZE..=Self::MAX_SIZE).text("Size"))
.changed()
{
self.set_size(size);
change = Some(SymbolToolChange::Size(self.size));
}
ui.separator();
for symbol in Symbol::ALL {
if ui
.selectable_label(self.symbol == symbol, symbol.name())
.clicked()
{
self.set_symbol(symbol);
change = Some(SymbolToolChange::Symbol(symbol));
ui.close();
}
}
});
change
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AxisScaleToolButton {
y_axis: bool,
scale: Scale,
}
impl AxisScaleToolButton {
pub fn x_axis() -> Self {
Self {
y_axis: false,
scale: Scale::Linear,
}
}
pub fn y_axis() -> Self {
Self {
y_axis: true,
scale: Scale::Linear,
}
}
pub fn scale(&self) -> Scale {
self.scale
}
pub fn set_scale(&mut self, scale: Scale) -> bool {
if scale != self.scale {
self.scale = scale;
true
} else {
false
}
}
fn axis_letter(&self) -> &'static str {
if self.y_axis { "Y" } else { "X" }
}
pub fn action_label(&self, scale: Scale) -> String {
let name = match scale {
Scale::Linear => "Linear",
Scale::Log10 => "Logarithmic",
};
format!("{name} {}-axis", self.axis_letter())
}
pub fn state_tooltip(&self, scale: Scale) -> String {
let name = match scale {
Scale::Linear => "linear",
Scale::Log10 => "logarithmic",
};
format!("{}-axis scale is {name}", self.axis_letter())
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<Scale> {
let mut changed = None;
let title = match self.scale {
Scale::Linear => "Lin",
Scale::Log10 => "Log",
};
ui.menu_button(title, |ui| {
for scale in [Scale::Linear, Scale::Log10] {
let resp = ui
.selectable_label(self.scale == scale, self.action_label(scale))
.on_hover_text(self.state_tooltip(scale));
if resp.clicked() {
if self.set_scale(scale) {
changed = Some(scale);
}
ui.close();
}
}
})
.response
.on_hover_text(self.state_tooltip(self.scale));
changed
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RulerToolButton {
active: bool,
}
impl RulerToolButton {
pub fn new() -> Self {
Self::default()
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn set_active(&mut self, active: bool) {
self.active = active;
}
pub fn toggle(&mut self) -> bool {
self.active = !self.active;
self.active
}
pub fn distance_text(start: [f64; 2], end: [f64; 2]) -> String {
let dx = end[0] - start[0];
let dy = end[1] - start[1];
let distance = (dx * dx + dy * dy).sqrt();
format!(" {distance:.1}px")
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<bool> {
let resp = ui
.selectable_label(self.active, "Ruler")
.on_hover_text("Measure the distance between two points of the plot");
if resp.clicked() {
return Some(self.toggle());
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_button_defaults_to_1d() {
assert_eq!(ProfileToolButton::new().dimension(), 1);
}
#[test]
fn profile_set_dimension_accepts_only_1_and_2() {
let mut b = ProfileToolButton::new();
assert!(!b.set_dimension(1));
assert!(b.set_dimension(2));
assert_eq!(b.dimension(), 2);
assert!(!b.set_dimension(0));
assert!(!b.set_dimension(3));
assert_eq!(b.dimension(), 2);
assert!(b.set_dimension(1));
assert_eq!(b.dimension(), 1);
}
#[test]
fn profile_labels_match_silx_state() {
assert_eq!(
ProfileToolButton::action_label(1),
"1D profile on visible image"
);
assert_eq!(
ProfileToolButton::action_label(2),
"2D profile on image stack"
);
assert_eq!(
ProfileToolButton::state_tooltip(1),
"1D profile is computed on visible image"
);
assert_eq!(
ProfileToolButton::state_tooltip(2),
"2D profile is computed, one 1D profile for each image in the stack"
);
}
#[test]
fn axis_scale_buttons_default_to_linear_and_track_changes() {
let mut x = AxisScaleToolButton::x_axis();
assert_eq!(x.scale(), Scale::Linear);
assert!(!x.set_scale(Scale::Linear));
assert!(x.set_scale(Scale::Log10));
assert_eq!(x.scale(), Scale::Log10);
}
#[test]
fn axis_scale_labels_match_silx_state() {
let x = AxisScaleToolButton::x_axis();
let y = AxisScaleToolButton::y_axis();
assert_eq!(x.action_label(Scale::Linear), "Linear X-axis");
assert_eq!(x.action_label(Scale::Log10), "Logarithmic X-axis");
assert_eq!(y.action_label(Scale::Linear), "Linear Y-axis");
assert_eq!(y.action_label(Scale::Log10), "Logarithmic Y-axis");
assert_eq!(x.state_tooltip(Scale::Linear), "X-axis scale is linear");
assert_eq!(y.state_tooltip(Scale::Log10), "Y-axis scale is logarithmic");
}
#[test]
fn symbol_button_defaults_to_circle_at_silx_size() {
let b = SymbolToolButton::new();
assert_eq!(b.symbol(), Symbol::Circle);
assert_eq!(b.size(), 6.0);
}
#[test]
fn symbol_set_size_clamps_to_silx_slider_range() {
let mut b = SymbolToolButton::new();
b.set_size(0.5);
assert_eq!(b.size(), SymbolToolButton::MIN_SIZE); b.set_size(99.0);
assert_eq!(b.size(), SymbolToolButton::MAX_SIZE); b.set_size(12.0);
assert_eq!(b.size(), 12.0);
}
#[test]
fn symbol_set_symbol_updates_selection() {
let mut b = SymbolToolButton::new();
b.set_symbol(Symbol::Diamond);
assert_eq!(b.symbol(), Symbol::Diamond);
}
#[test]
fn ruler_button_defaults_inactive_and_toggles() {
let mut b = RulerToolButton::new();
assert!(!b.is_active(), "silx ruler button starts unchecked");
assert!(b.toggle(), "toggle activates");
assert!(b.is_active());
assert!(!b.toggle(), "toggle deactivates");
assert!(!b.is_active());
b.set_active(true);
assert!(b.is_active());
}
#[test]
fn ruler_distance_text_matches_silx_format() {
assert_eq!(
RulerToolButton::distance_text([0.0, 0.0], [3.0, 4.0]),
" 5.0px"
);
assert_eq!(
RulerToolButton::distance_text([2.0, 7.0], [2.0, 7.0]),
" 0.0px"
);
assert_eq!(
RulerToolButton::distance_text([4.0, 4.0], [0.0, 1.0]),
" 5.0px"
);
assert_eq!(
RulerToolButton::distance_text([0.0, 0.0], [1.0, 1.0]),
" 1.4px"
);
}
}