| 154 |
154 |
|
//! looks for it, and because the silent version reads as "egui does not need
|
| 155 |
155 |
|
//! a palette" rather than "nobody has built the renderer yet".
|
| 156 |
156 |
|
|
|
157 |
+ |
//! # 0.33.0: an interval is a sixth control shape
|
|
158 |
+ |
//!
|
|
159 |
+ |
//! `makeover-layout` 0.34.0's [`FieldKind::Interval`], drawn as `Control::Spanned`:
|
|
160 |
+ |
//! two drag boxes on one row with the word `to` between them.
|
|
161 |
+ |
//!
|
|
162 |
+ |
//! - **Dragged rather than typed**, because that is what these controls already
|
|
163 |
+ |
//! were. audiofiles' six filter axes are `DragValue` pairs sharing an extent,
|
|
164 |
+ |
//! a speed and a suffix, and describing them into two text boxes would be a
|
|
165 |
+ |
//! port that cost the app a control.
|
|
166 |
+ |
//! - **One row, not two wells stacked.** Two wells are two questions on screen
|
|
167 |
+ |
//! whatever the description says, and the arrangement is the whole content of
|
|
168 |
+ |
//! the kind.
|
|
169 |
+ |
//! - **An empty end reads as the bound it stands for.** An unset minimum sits
|
|
170 |
+ |
//! on the low edge and stores no filter, which is what the shipped control
|
|
171 |
+ |
//! did; egui's `DragValue` has no empty state, and a text box in its place
|
|
172 |
+ |
//! would be the regression above. With no extent to fall back on it reads
|
|
173 |
+ |
//! zero -- the one number this renderer invents, invented where the
|
|
174 |
+ |
//! description declined to say anything.
|
|
175 |
+ |
//! - **The word rather than a dash**, which on a signed axis is a minus sign.
|
|
176 |
+ |
//! audiofiles filters loudness in dBFS.
|
|
177 |
+ |
//!
|
|
178 |
+ |
//! `Axis` holds the four facts both boxes share, because they are one axis:
|
|
179 |
+ |
//! reading `min`, `max`, `step` and `unit` once is what stops the two ends
|
|
180 |
+ |
//! drifting apart.
|
|
181 |
+ |
//!
|
| 157 |
182 |
|
//! # 0.32.0: a number draws its unit
|
| 158 |
183 |
|
//!
|
| 159 |
184 |
|
//! `makeover-layout` 0.33.0's [`Field::unit`], and this host is the one the
|
| 212 |
237 |
|
#![forbid(unsafe_code)]
|
| 213 |
238 |
|
|
| 214 |
239 |
|
use egui::{
|
| 215 |
|
- |
Color32, ComboBox, CornerRadius, Margin, Painter, Rect, Response, RichText, Shape, Slider,
|
| 216 |
|
- |
Stroke, TextEdit, Ui,
|
|
240 |
+ |
Color32, ComboBox, CornerRadius, DragValue, Margin, Painter, Rect, Response, RichText, Shape,
|
|
241 |
+ |
Slider, Stroke, TextEdit, Ui,
|
| 217 |
242 |
|
};
|
| 218 |
243 |
|
use makeover_layout::{Bevel, Choice, Depth, Edge, Field, FieldKind, Fill, State, Tone};
|
| 219 |
244 |
|
use std::ops::RangeInclusive;
|
| 585 |
610 |
|
Text(&'a mut String),
|
| 586 |
611 |
|
/// A checkbox, on or off.
|
| 587 |
612 |
|
On(&'a mut bool),
|
|
613 |
+ |
/// The two buffers behind a [`FieldKind::Interval`], lower first.
|
|
614 |
+ |
///
|
|
615 |
+ |
/// Two buffers rather than one string with a separator, which is
|
|
616 |
+ |
/// [`makeover_layout::Field::upper_name`]'s reason one level down: an
|
|
617 |
+ |
/// interval is submitted under two names, so it is edited as two values,
|
|
618 |
+ |
/// and a delimiter this crate owned could appear inside either of them.
|
|
619 |
+ |
///
|
|
620 |
+ |
/// Either end may be empty while the other stands. An open end is an
|
|
621 |
+ |
/// answer -- "over 120 BPM" -- rather than a half-filled box.
|
|
622 |
+ |
///
|
|
623 |
+ |
/// Added 0.33.0 with makeover-layout 0.34.0.
|
|
624 |
+ |
Between {
|
|
625 |
+ |
/// The lower end's buffer.
|
|
626 |
+ |
lower: &'a mut String,
|
|
627 |
+ |
/// The upper end's buffer.
|
|
628 |
+ |
upper: &'a mut String,
|
|
629 |
+ |
},
|
| 588 |
630 |
|
}
|
| 589 |
631 |
|
|
| 590 |
632 |
|
/// The label, marked if the field is compulsory.
|
| 626 |
668 |
|
/// question means, so a well with a figure in it is not a quieter version
|
| 627 |
669 |
|
/// of this control, it is a different one.
|
| 628 |
670 |
|
Slid,
|
|
671 |
+ |
/// Two values dragged across one axis, drawn as one question.
|
|
672 |
+ |
///
|
|
673 |
+ |
/// Apart from [`Typed`](Self::Typed) for the reason
|
|
674 |
+ |
/// [`FieldKind::Interval`] is apart from `Number`: two wells one under the
|
|
675 |
+ |
/// other are two questions on screen, whatever the description says, and
|
|
676 |
+ |
/// the arrangement is the whole content of the kind.
|
|
677 |
+ |
Spanned,
|
| 629 |
678 |
|
}
|
| 630 |
679 |
|
|
| 631 |
680 |
|
/// Which shape a kind takes.
|
| 663 |
712 |
|
FieldKind::Radio => Control::Listed,
|
| 664 |
713 |
|
FieldKind::Checkbox => Control::Toggled,
|
| 665 |
714 |
|
FieldKind::Range => Control::Slid,
|
|
715 |
+ |
FieldKind::Interval => Control::Spanned,
|
| 666 |
716 |
|
_ => Control::Typed,
|
| 667 |
717 |
|
}
|
| 668 |
718 |
|
}
|
| 787 |
837 |
|
}
|
| 788 |
838 |
|
}
|
| 789 |
839 |
|
|
|
840 |
+ |
/// Which end of an interval a box is.
|
|
841 |
+ |
///
|
|
842 |
+ |
/// Named rather than a bool, because what it selects is not a side but a
|
|
843 |
+ |
/// fallback: an empty end reads as the bound it stands for, and which bound
|
|
844 |
+ |
/// that is depends on the end.
|
|
845 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
846 |
+ |
enum Bound {
|
|
847 |
+ |
/// The lower end, falling back to the start of the extent.
|
|
848 |
+ |
Low,
|
|
849 |
+ |
/// The upper end, falling back to its end.
|
|
850 |
+ |
High,
|
|
851 |
+ |
}
|
|
852 |
+ |
|
|
853 |
+ |
/// The facts an interval's two boxes share.
|
|
854 |
+ |
///
|
|
855 |
+ |
/// One struct because they are one axis: [`Field::min`], [`Field::max`],
|
|
856 |
+ |
/// [`Field::step`] and [`Field::unit`] describe the question rather than either
|
|
857 |
+ |
/// end of it, so reading them once is what stops the two boxes drifting apart.
|
|
858 |
+ |
struct Axis<'a> {
|
|
859 |
+ |
/// The extent both ends are dragged inside, when it is one this host can
|
|
860 |
+ |
/// read.
|
|
861 |
+ |
extent: Option<RangeInclusive<f64>>,
|
|
862 |
+ |
/// The granularity, as the description writes it.
|
|
863 |
+ |
step: Option<&'a str>,
|
|
864 |
+ |
/// What the axis is measured in.
|
|
865 |
+ |
unit: Option<&'a str>,
|
|
866 |
+ |
}
|
|
867 |
+ |
|
|
868 |
+ |
impl Axis<'_> {
|
|
869 |
+ |
/// What an empty end reads as: the bound it stands for.
|
|
870 |
+ |
///
|
|
871 |
+ |
/// Zero with no extent to fall back on. That is the one number this
|
|
872 |
+ |
/// renderer invents, and it invents it where the description declined to
|
|
873 |
+ |
/// say anything: an unbounded interval has no edge for the end to sit on,
|
|
874 |
+ |
/// and a drag box has to start somewhere.
|
|
875 |
+ |
fn edge(&self, which: Bound) -> f64 {
|
|
876 |
+ |
self.extent.as_ref().map_or(0.0, |extent| match which {
|
|
877 |
+ |
Bound::Low => *extent.start(),
|
|
878 |
+ |
Bound::High => *extent.end(),
|
|
879 |
+ |
})
|
|
880 |
+ |
}
|
|
881 |
+ |
|
|
882 |
+ |
/// One end of the interval, as a drag box.
|
|
883 |
+ |
///
|
|
884 |
+ |
/// # An empty end reads as its bound
|
|
885 |
+ |
///
|
|
886 |
+ |
/// Which is what the shipped control did before it was described: an unset
|
|
887 |
+ |
/// minimum sits on the low edge and stores no filter. egui's `DragValue`
|
|
888 |
+ |
/// holds a number and has no empty state to offer, so the alternative was a
|
|
889 |
+ |
/// text box, and that would cost the app a control on the way into being
|
|
890 |
+ |
/// described.
|
|
891 |
+ |
///
|
|
892 |
+ |
/// With no extent to fall back on, an empty end reads zero. That is the one
|
|
893 |
+ |
/// number this renderer invents, and it invents it where the description
|
|
894 |
+ |
/// declined to say anything: an unbounded interval has no edge for the end
|
|
895 |
+ |
/// to sit on, and a drag box has to start somewhere.
|
|
896 |
+ |
///
|
|
897 |
+ |
/// Nothing is written back until the user drags, so a value the app put
|
|
898 |
+ |
/// there survives being looked at -- the same guarantee the slider makes.
|
|
899 |
+ |
fn end(&self, ui: &mut Ui, value: &mut String, which: Bound) -> Response {
|
|
900 |
+ |
let mut number = value.parse::<f64>().unwrap_or(self.edge(which));
|
|
901 |
+ |
let mut drag = DragValue::new(&mut number);
|
|
902 |
+ |
if let Some(extent) = self.extent.clone() {
|
|
903 |
+ |
drag = drag.range(extent);
|
|
904 |
+ |
}
|
|
905 |
+ |
if let Some(places) = decimals(self.step) {
|
|
906 |
+ |
drag = drag.max_decimals(places);
|
|
907 |
+ |
}
|
|
908 |
+ |
if let Some(step) = self.step.and_then(|s| s.parse::<f64>().ok()) {
|
|
909 |
+ |
drag = drag.speed(step);
|
|
910 |
+ |
}
|
|
911 |
+ |
// Inside the control, beside the readout, which is where `Field::unit`
|
|
912 |
+ |
// was decided to belong and where these boxes already put it.
|
|
913 |
+ |
if let Some(unit) = self.unit {
|
|
914 |
+ |
drag = drag.suffix(format!(" {unit}"));
|
|
915 |
+ |
}
|
|
916 |
+ |
let response = ui.add(drag);
|
|
917 |
+ |
if response.changed() {
|
|
918 |
+ |
*value = match decimals(self.step) {
|
|
919 |
+ |
Some(places) => format!("{number:.places$}"),
|
|
920 |
+ |
None => number.to_string(),
|
|
921 |
+ |
};
|
|
922 |
+ |
}
|
|
923 |
+ |
response
|
|
924 |
+ |
}
|
|
925 |
+ |
}
|
|
926 |
+ |
|
| 790 |
927 |
|
/// The control alone, without its label, hint or error.
|
| 791 |
928 |
|
fn control(
|
| 792 |
929 |
|
ui: &mut Ui,
|
| 800 |
937 |
|
// described — visible on screen, in the way an empty select is at the
|
| 801 |
938 |
|
// webview renderer, rather than reported in a log nobody reads.
|
| 802 |
939 |
|
let mut discard = String::new();
|
|
940 |
+ |
// The interval's second scratch buffer. Two ends means the mismatch path
|
|
941 |
+ |
// needs two places to write nothing to.
|
|
942 |
+ |
let mut spare = String::new();
|
| 803 |
943 |
|
let mut off = false;
|
| 804 |
944 |
|
|
| 805 |
945 |
|
match shape_of(field) {
|
| 855 |
995 |
|
}
|
| 856 |
996 |
|
response
|
| 857 |
997 |
|
}
|
|
998 |
+ |
// One question, so one row. Two wells stacked would be two questions on
|
|
999 |
+ |
// screen whatever the description said, which is the reading
|
|
1000 |
+ |
// `FieldKind::Interval` exists to prevent.
|
|
1001 |
+ |
//
|
|
1002 |
+ |
// Dragged rather than typed, because that is what these controls
|
|
1003 |
+ |
// already were: audiofiles' six filter axes are `DragValue` pairs with
|
|
1004 |
+ |
// a shared extent, a speed and a suffix, and a port that turned them
|
|
1005 |
+ |
// into text boxes would be a description costing the app a control.
|
|
1006 |
+ |
Control::Spanned => {
|
|
1007 |
+ |
let (lower, upper) = match filling {
|
|
1008 |
+ |
Filling::Between { lower, upper } => (lower, upper),
|
|
1009 |
+ |
_ => (&mut discard, &mut spare),
|
|
1010 |
+ |
};
|
|
1011 |
+ |
let axis = Axis {
|
|
1012 |
+ |
extent: extent(field),
|
|
1013 |
+ |
step: field.step,
|
|
1014 |
+ |
unit: unit_of(field),
|
|
1015 |
+ |
};
|
|
1016 |
+ |
ui.horizontal(|ui| {
|
|
1017 |
+ |
let low = axis.end(ui, lower, Bound::Low);
|
|
1018 |
+ |
// The word rather than a dash. A dash between two numbers is a
|
|
1019 |
+ |
// minus sign on a signed axis, and audiofiles filters loudness
|
|
1020 |
+ |
// in dBFS.
|
|
1021 |
+ |
ui.label(RichText::new("to").color(palette.content_secondary));
|
|
1022 |
+ |
let high = axis.end(ui, upper, Bound::High);
|
|
1023 |
+ |
low | high
|
|
1024 |
+ |
})
|
|
1025 |
+ |
.inner
|
|
1026 |
+ |
}
|
| 858 |
1027 |
|
Control::Typed => {
|
| 859 |
1028 |
|
let text = match filling {
|
| 860 |
1029 |
|
Filling::Text(text) => text,
|
| 1312 |
1481 |
|
assert_eq!(extent(&dated), None);
|
| 1313 |
1482 |
|
}
|
| 1314 |
1483 |
|
|
|
1484 |
+ |
#[test]
|
|
1485 |
+ |
fn an_interval_is_its_own_shape_and_not_two_numbers() {
|
|
1486 |
+ |
// The distinction the kind was added for, at the renderer that has to
|
|
1487 |
+ |
// arrange it: two wells stacked are two questions on screen, whatever
|
|
1488 |
+ |
// the description says.
|
|
1489 |
+ |
assert_eq!(control_shape(FieldKind::Interval), Control::Spanned);
|
|
1490 |
+ |
assert_eq!(shape_of(&Field::interval("a", "b", "A")), Control::Spanned);
|
|
1491 |
+ |
|
|
1492 |
+ |
// Unlike a range, it owes no extent: its bounds are a rule on each end
|
|
1493 |
+ |
// rather than the control, so a missing one is an open end.
|
|
1494 |
+ |
let axis = Field {
|
|
1495 |
+ |
min: Some("0"),
|
|
1496 |
+ |
max: Some("300"),
|
|
1497 |
+ |
..Field::interval("bpm_min", "bpm_max", "BPM")
|
|
1498 |
+ |
};
|
|
1499 |
+ |
assert_eq!(shape_of(&axis), Control::Spanned);
|
|
1500 |
+ |
}
|
|
1501 |
+ |
|
|
1502 |
+ |
#[test]
|
|
1503 |
+ |
fn an_empty_end_reads_as_the_bound_it_stands_for() {
|
|
1504 |
+ |
// Which is what the shipped control did before it was described: an
|
|
1505 |
+ |
// unset minimum sits on the low edge and stores no filter. `DragValue`
|
|
1506 |
+ |
// has no empty state, and a text box instead would cost the app a
|
|
1507 |
+ |
// control on the way into being described.
|
|
1508 |
+ |
let axis = Axis {
|
|
1509 |
+ |
extent: Some(0.0..=300.0),
|
|
1510 |
+ |
step: Some("1"),
|
|
1511 |
+ |
unit: Some("BPM"),
|
|
1512 |
+ |
};
|
|
1513 |
+ |
assert!((axis.edge(Bound::Low) - 0.0).abs() < f64::EPSILON);
|
|
1514 |
+ |
assert!((axis.edge(Bound::High) - 300.0).abs() < f64::EPSILON);
|
|
1515 |
+ |
|
|
1516 |
+ |
// With no extent there is no edge to sit on, and a drag box has to
|
|
1517 |
+ |
// start somewhere. The one number this renderer invents, invented where
|
|
1518 |
+ |
// the description declined to say anything.
|
|
1519 |
+ |
let open = Axis {
|
|
1520 |
+ |
extent: None,
|
|
1521 |
+ |
step: None,
|
|
1522 |
+ |
unit: None,
|
|
1523 |
+ |
};
|
|
1524 |
+ |
assert!((open.edge(Bound::Low) - 0.0).abs() < f64::EPSILON);
|
|
1525 |
+ |
assert!((open.edge(Bound::High) - 0.0).abs() < f64::EPSILON);
|
|
1526 |
+ |
}
|
|
1527 |
+ |
|
| 1315 |
1528 |
|
#[test]
|
| 1316 |
1529 |
|
fn the_step_decides_how_a_dragged_value_is_written_back() {
|
| 1317 |
1530 |
|
// Without it a 0-to-1 threshold writes back whatever float the drag
|