From 9968afbe4810665c622664baaa6f76e43890607a Mon Sep 17 00:00:00 2001 From: sacha-renault Date: Sat, 5 Jul 2025 21:45:57 +0200 Subject: [PATCH 1/3] feat: Add with_sweep_range() method for control over knob start position and rotation range. Maintains backward compatibility --- examples/example_knob.rs | 6 ++++-- src/lib.rs | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/examples/example_knob.rs b/examples/example_knob.rs index f438ad2..11a0690 100644 --- a/examples/example_knob.rs +++ b/examples/example_knob.rs @@ -117,7 +117,8 @@ impl eframe::App for KnobExample { egui::Color32::from_rgb(100, 255, 100), ) .with_size(50.0) - .with_label_format(|v| format!("{:.2}%", v)), + .with_label_format(|v| format!("{:.2}%", v)) + .with_sweep_range(1. / 8., 0.75), ); ui.add( @@ -128,7 +129,8 @@ impl eframe::App for KnobExample { egui::Color32::from_rgb(50, 50, 220), egui::Color32::from_rgb(100, 100, 255), ) - .with_size(50.0), + .with_size(50.0) + .with_sweep_range(0., 2.), ); }); }); diff --git a/src/lib.rs b/src/lib.rs index c1b687b..5828620 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +use core::f32; + use egui::{Align2, Color32, Rect, Response, Sense, Stroke, Ui, Vec2, Widget}; /// Position of the label relative to the knob @@ -42,6 +44,8 @@ pub struct Knob<'a> { label_offset: f32, label_format: Box String>, step: Option, + min_angle: f32, + max_angle: f32, } impl<'a> Knob<'a> { @@ -69,9 +73,37 @@ impl<'a> Knob<'a> { label_offset: 1.0, label_format: Box::new(|v| format!("{:.2}", v)), step: None, + min_angle: -std::f32::consts::PI, + max_angle: std::f32::consts::PI * 0.5, } } + /// 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 bottom-left (135°) and sweep 270° clockwise to bottom-right. + /// + /// # 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 + pub fn with_sweep_range(mut self, start_angle_normalized: f32, range: f32) -> Self { + self.min_angle = + start_angle_normalized.rem_euclid(1.) * f32::consts::PI * 2. + f32::consts::PI / 2.; + + // A range of 1. represent a full turn + self.max_angle = self.min_angle + range.max(0.) * 2. * f32::consts::PI; + self + } + /// Sets the size of the knob pub fn with_size(mut self, size: f32) -> Self { self.size = size; @@ -210,8 +242,8 @@ impl Widget for Knob<'_> { let center = knob_rect.center(); let radius = knob_size.x / 2.0; - let angle = (*self.value - self.min) / (self.max - self.min) * std::f32::consts::PI * 1.5 - - std::f32::consts::PI; + let angle = self.min_angle + + (*self.value - self.min) / (self.max - self.min) * (self.max_angle - self.min_angle); painter.circle_stroke( center, From 2a68f53a37c1ff364a14d6dc72749fcca812ca79 Mon Sep 17 00:00:00 2001 From: sacha-renault Date: Sat, 5 Jul 2025 21:52:05 +0200 Subject: [PATCH 2/3] docs: update changelog for sweep range feature --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af1e8f4..3d0859f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### 🚀 Features + +- Add configurable sweep range with `with_sweep_range()` method + ## [0.1.9] - 2025-02-04 ### 🚀 Features From c105722eb426c174d21e69eafd352f82caf1df49 Mon Sep 17 00:00:00 2001 From: sacha-renault Date: Sun, 6 Jul 2025 09:36:29 +0200 Subject: [PATCH 3/3] fix: Address PR feedback Prevent division by zero when min equals max value Also improves angle calculations using TAU constant and adds NaN validation. Also fix doc --- examples/example_knob.rs | 2 +- src/lib.rs | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/examples/example_knob.rs b/examples/example_knob.rs index 11a0690..2f7595a 100644 --- a/examples/example_knob.rs +++ b/examples/example_knob.rs @@ -122,7 +122,7 @@ impl eframe::App for KnobExample { ); ui.add( - Knob::new(&mut self.blue_value, 0.0, 100.0, egui_knob::KnobStyle::Dot) + Knob::new(&mut self.blue_value, 0.0, 100., egui_knob::KnobStyle::Dot) .with_label("Top", egui_knob::LabelPosition::Top) .with_colors( egui::Color32::from_rgb(30, 30, 80), diff --git a/src/lib.rs b/src/lib.rs index 5828620..f772cd9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,16 @@ pub struct Knob<'a> { label_offset: f32, label_format: Box String>, step: Option, + + /// Minimum angle in radians. + /// Specifies the lower bound of the knob's rotation. + /// Expected range: 0.0 to TAU. Values outside this range are allowed if your use case requires it. min_angle: f32, + + /// Maximum angle in radians. + /// Specifies the upper bound of the knob's rotation. + /// Can be any value > `min_angle` + /// range `max_angle` - `min_angle` > TAU is allowed but will induce multi-turn max_angle: f32, } @@ -73,6 +82,8 @@ impl<'a> Knob<'a> { label_offset: 1.0, label_format: Box::new(|v| format!("{:.2}", v)), step: None, + + // Hardcode those two angles to ENSURE backward compatibility min_angle: -std::f32::consts::PI, max_angle: std::f32::consts::PI * 0.5, } @@ -81,7 +92,7 @@ impl<'a> Knob<'a> { /// 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 bottom-left (135°) and sweep 270° clockwise to bottom-right. + /// knobs start at the left (180°) and sweep 270° clockwise to bottom. /// /// # Arguments /// * `start_angle_normalized` - Starting position as fraction of full circle: @@ -95,12 +106,20 @@ impl<'a> Knob<'a> { /// - `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() { + // Invalid input, return unchanged + return self; + } + self.min_angle = - start_angle_normalized.rem_euclid(1.) * f32::consts::PI * 2. + f32::consts::PI / 2.; + start_angle_normalized.rem_euclid(1.) * f32::consts::TAU + f32::consts::PI / 2.; // A range of 1. represent a full turn - self.max_angle = self.min_angle + range.max(0.) * 2. * f32::consts::PI; + self.max_angle = self.min_angle + range.max(0.) * f32::consts::TAU; self } @@ -242,8 +261,15 @@ impl Widget for Knob<'_> { let center = knob_rect.center(); let radius = knob_size.x / 2.0; - let angle = self.min_angle - + (*self.value - self.min) / (self.max - self.min) * (self.max_angle - self.min_angle); + let angle = if self.min == self.max { + // If min == max, just return min angle + // That's a edge case, using a 0 range knob is pretty useless + self.min_angle + } else { + self.min_angle + + (*self.value - self.min) / (self.max - self.min) + * (self.max_angle - self.min_angle) + }; painter.circle_stroke( center,