mirror of
https://github.com/obsqrbtz/egui_knob.git
synced 2026-04-08 20:19:17 +03:00
254 lines
8.3 KiB
Rust
254 lines
8.3 KiB
Rust
use egui::{remap, Color32, Response, Sense, Ui, Widget};
|
|
|
|
use crate::config::KnobConfig;
|
|
use crate::render::KnobRenderer;
|
|
use crate::style::{KnobStyle, LabelPosition};
|
|
|
|
pub struct Knob<'a> {
|
|
pub(crate) value: &'a mut f32,
|
|
pub(crate) min: f32,
|
|
pub(crate) max: f32,
|
|
pub(crate) config: KnobConfig,
|
|
}
|
|
|
|
impl<'a> Knob<'a> {
|
|
/// Creates a new knob widget
|
|
///
|
|
/// # Arguments
|
|
/// * `value` - Mutable reference to the value controlled by the knob
|
|
/// * `min` - Minimum value
|
|
/// * `max` - Maximum value
|
|
/// * `style` - Visual style of the knob indicator
|
|
pub fn new(value: &'a mut f32, min: f32, max: f32, style: KnobStyle) -> Self {
|
|
Self {
|
|
value,
|
|
min,
|
|
max,
|
|
config: KnobConfig::new(style),
|
|
}
|
|
}
|
|
|
|
/// Sets the angular sweep range of the knob
|
|
///
|
|
/// This controls where the knob starts and how far it can rotate. By default,
|
|
/// knobs start at the left (180°) and sweep 270° clockwise to bottom.
|
|
///
|
|
/// # Arguments
|
|
/// * `start_angle_normalized` - Starting position as fraction of full circle:
|
|
/// - `0.0` = bottom (6 o'clock)
|
|
/// - `0.25` = left (9 o'clock)
|
|
/// - `0.5` = top (12 o'clock)
|
|
/// - `0.75` = right (3 o'clock)
|
|
/// * `range` - How far the knob can sweep as fraction of full circle:
|
|
/// - `0.25` = quarter turn (90°)
|
|
/// - `0.5` = half turn (180°)
|
|
/// - `0.75` = three-quarter turn (270°)
|
|
/// - `1.0` = full turn (360°)
|
|
/// - Values > 1.0 create multi-turn knobs
|
|
/// - Negative values are clamped to 0.0
|
|
///
|
|
/// Note: the start angle is offset by PI/2 so that `0.0` is at the bottom (6 o'clock)
|
|
pub fn with_sweep_range(mut self, start_angle_normalized: f32, range: f32) -> Self {
|
|
if start_angle_normalized.is_nan() || range.is_nan() {
|
|
return self;
|
|
}
|
|
|
|
self.config.min_angle =
|
|
start_angle_normalized.rem_euclid(1.0) * std::f32::consts::TAU + std::f32::consts::PI / 2.0;
|
|
self.config.max_angle = self.config.min_angle + range.max(0.0) * std::f32::consts::TAU;
|
|
self
|
|
}
|
|
|
|
/// Sets the size of the knob
|
|
pub fn with_size(mut self, size: f32) -> Self {
|
|
self.config.size = size;
|
|
self
|
|
}
|
|
|
|
/// Sets the font size for the label
|
|
pub fn with_font_size(mut self, size: f32) -> Self {
|
|
self.config.font_size = size;
|
|
self
|
|
}
|
|
|
|
/// Sets the stroke width for the knob's outline and indicator
|
|
pub fn with_stroke_width(mut self, width: f32) -> Self {
|
|
self.config.stroke_width = width;
|
|
self
|
|
}
|
|
|
|
/// Sets the colors for different parts of the knob
|
|
///
|
|
/// # Arguments
|
|
/// * `knob_color` - Color of the knob's outline
|
|
/// * `line_color` - Color of the indicator
|
|
/// * `text_color` - Color of the label text
|
|
pub fn with_colors(
|
|
mut self,
|
|
knob_color: Color32,
|
|
line_color: Color32,
|
|
text_color: Color32,
|
|
) -> Self {
|
|
self.config.colors.knob_color = knob_color;
|
|
self.config.colors.line_color = line_color;
|
|
self.config.colors.text_color = text_color;
|
|
self
|
|
}
|
|
|
|
/// Adds a label to the knob
|
|
///
|
|
/// # Arguments
|
|
/// * `label` - Text to display
|
|
/// * `position` - Position of the label relative to the knob
|
|
pub fn with_label(mut self, label: impl Into<String>, position: LabelPosition) -> Self {
|
|
self.config.label = Some(label.into());
|
|
self.config.label_position = position;
|
|
self
|
|
}
|
|
|
|
/// Sets the spacing between the knob and its label
|
|
pub fn with_label_offset(mut self, offset: f32) -> Self {
|
|
self.config.label_offset = offset;
|
|
self
|
|
}
|
|
|
|
/// Sets a custom format function for displaying the value
|
|
///
|
|
/// # Example
|
|
/// ```no_run
|
|
/// use egui_knob::{Knob, KnobStyle};
|
|
/// ui.add(
|
|
/// Knob::new(&mut value, 0.0, 1.0, KnobStyle::Wiper)
|
|
/// .with_label_format(|v| format!("{:.1}%", v * 100.0))
|
|
/// );
|
|
/// ```
|
|
pub fn with_label_format(mut self, format: impl Fn(f32) -> String + 'static) -> Self {
|
|
self.config.label_format = Box::new(format);
|
|
self
|
|
}
|
|
|
|
/// Sets the step size for value changes
|
|
pub fn with_step(mut self, step: Option<f32>) -> Self {
|
|
self.config.step = step;
|
|
self
|
|
}
|
|
|
|
/// Controls whether to show the background arc indicating the full range
|
|
pub fn with_background_arc(mut self, enabled: bool) -> Self {
|
|
self.config.show_background_arc = enabled;
|
|
self
|
|
}
|
|
|
|
/// Controls whether to show the filled segment on the background arc
|
|
///
|
|
/// When enabled (and background arc is visible), displays a colored segment
|
|
/// from the minimum position to the current value position.
|
|
pub fn with_show_filled_segments(mut self, enabled: bool) -> Self {
|
|
self.config.show_filled_segments = enabled;
|
|
self
|
|
}
|
|
|
|
/// Sets the drag sensitivity for mouse interactions
|
|
///
|
|
/// Default is 0.005.
|
|
pub fn with_drag_sensitivity(mut self, sensitivity: f32) -> Self {
|
|
self.config.drag_sensitivity = sensitivity;
|
|
self
|
|
}
|
|
|
|
/// Sets a reset value to return to on doubleclick event.
|
|
pub fn with_double_click_reset(mut self, reset_value: f32) -> Self {
|
|
self.config.reset_value = Some(reset_value);
|
|
self
|
|
}
|
|
|
|
/// Allows user to use scroll wheel to change knob value
|
|
/// Uses config.step for the increment value
|
|
pub fn with_middle_scroll(mut self) -> Self {
|
|
self.config.allow_scroll = true;
|
|
self
|
|
}
|
|
pub fn with_logarithmic_scaling(mut self) -> Self {
|
|
self.config.logarithmic_scaling = true;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Widget for Knob<'_> {
|
|
fn ui(self, ui: &mut Ui) -> Response {
|
|
if self.value.is_nan() {
|
|
*self.value = self.min;
|
|
}
|
|
|
|
let mut raw = if self.config.logarithmic_scaling {
|
|
remap(*self.value, self.min..=self.max, 1.0..=10.0).log(10.0)
|
|
} else {
|
|
remap(*self.value, self.min..=self.max, 0.0..=1.0)
|
|
};
|
|
|
|
let renderer = KnobRenderer::new(&self.config, *self.value, raw, self.min, self.max);
|
|
let adjusted_size = renderer.calculate_size(ui);
|
|
|
|
let (rect, response) = ui.allocate_exact_size(adjusted_size, Sense::click_and_drag());
|
|
|
|
let mut response = response;
|
|
if response.dragged() {
|
|
let delta = response.drag_delta().y;
|
|
let step = self.config.step.unwrap_or(self.config.drag_sensitivity);
|
|
raw = (raw - delta * step).clamp(0.0,1.0);
|
|
|
|
raw = if let Some(step) = self.config.step {
|
|
let steps = (raw / step).round();
|
|
(steps * step).clamp(0.0, 1.0)
|
|
} else {
|
|
raw
|
|
};
|
|
|
|
if self.value.is_nan() {
|
|
*self.value = 0.0;
|
|
}
|
|
|
|
response.mark_changed();
|
|
} else if response.hovered() & self.config.allow_scroll {
|
|
if let Some(scoll) = ui.input(|input| {
|
|
input.events.iter().find_map(|e| match e {
|
|
egui::Event::MouseWheel { delta, .. } => Some(*delta),
|
|
_ => None,
|
|
})
|
|
}) {
|
|
raw = (raw
|
|
+ scoll.y * self.config.step.unwrap_or(self.config.drag_sensitivity))
|
|
.clamp(0.0, 1.0);
|
|
}
|
|
}
|
|
|
|
*self.value = if self.config.logarithmic_scaling {
|
|
remap(10f32.powf(raw), 1.0..=10.0, self.min..=self.max)
|
|
}else {
|
|
remap(raw, 0.0..=1.0, self.min..=self.max)
|
|
};
|
|
|
|
if response.double_clicked() {
|
|
if let Some(reset_value) = self.config.reset_value {
|
|
*self.value = reset_value
|
|
}
|
|
}
|
|
|
|
let knob_rect = renderer.calculate_knob_rect(rect);
|
|
let center = knob_rect.center();
|
|
let radius = self.config.size / 2.0;
|
|
|
|
let updated_renderer = KnobRenderer::new(&self.config, *self.value, raw, self.min, self.max);
|
|
updated_renderer.render_knob(ui.painter(), center, radius, response.hovered());
|
|
updated_renderer.render_label(ui, rect);
|
|
|
|
if self.config.label.is_some() && response.hovered() {
|
|
response
|
|
.clone()
|
|
.on_hover_text((self.config.label_format)(*self.value));
|
|
}
|
|
|
|
response
|
|
}
|
|
}
|