| 240 |
240 |
|
Color32, ComboBox, CornerRadius, DragValue, Margin, Painter, Rect, Response, RichText, Shape,
|
| 241 |
241 |
|
Slider, Stroke, TextEdit, Ui,
|
| 242 |
242 |
|
};
|
| 243 |
|
- |
use makeover_layout::{Bevel, Choice, Depth, Edge, Field, FieldKind, Fill, State, Tone};
|
|
243 |
+ |
use makeover_layout::{
|
|
244 |
+ |
Bevel, Choice, Depth, Edge, Field, FieldKind, Fill, State, ThemeVariant, Tone,
|
|
245 |
+ |
};
|
| 244 |
246 |
|
use std::ops::RangeInclusive;
|
| 245 |
247 |
|
|
| 246 |
248 |
|
/// Columns, narrowing, cell parts and the sort caret, over `egui_extras`.
|
| 668 |
670 |
|
/// question means, so a well with a figure in it is not a quieter version
|
| 669 |
671 |
|
/// of this control, it is a different one.
|
| 670 |
672 |
|
Slid,
|
|
673 |
+ |
/// Picked from a list of themes that arrives grouped and marked.
|
|
674 |
+ |
///
|
|
675 |
+ |
/// Apart from [`Chosen`](Self::Chosen) rather than folded into it, and the
|
|
676 |
+ |
/// distinction is the same one [`Listed`](Self::Listed) draws: it is not a
|
|
677 |
+ |
/// different question, it is a different amount of structure on screen. A
|
|
678 |
+ |
/// theme picker's rows carry a group heading and a contrast mark, and both
|
|
679 |
+ |
/// come from members [`Field::options`] does not have, so a shared arm
|
|
680 |
+ |
/// would be a `matches!` on the kind inside the loop rather than one arm
|
|
681 |
+ |
/// less.
|
|
682 |
+ |
///
|
|
683 |
+ |
/// Added with makeover-layout 0.38.0's [`FieldKind::Theme`].
|
|
684 |
+ |
Themed,
|
| 671 |
685 |
|
/// Two values dragged across one axis, drawn as one question.
|
| 672 |
686 |
|
///
|
| 673 |
687 |
|
/// Apart from [`Typed`](Self::Typed) for the reason
|
| 721 |
735 |
|
FieldKind::Checkbox => Control::Toggled,
|
| 722 |
736 |
|
FieldKind::Range => Control::Slid,
|
| 723 |
737 |
|
FieldKind::Interval => Control::Spanned,
|
|
738 |
+ |
FieldKind::Theme => Control::Themed,
|
| 724 |
739 |
|
_ => Control::Typed,
|
| 725 |
740 |
|
}
|
| 726 |
741 |
|
}
|
| 1172 |
1187 |
|
})
|
| 1173 |
1188 |
|
.response
|
| 1174 |
1189 |
|
}
|
|
1190 |
+ |
// The grouping comes out of the order rather than out of a group list:
|
|
1191 |
+ |
// `Field::themes` arrives sorted by variant, so the run of one variant
|
|
1192 |
+ |
// is the group and a heading opens whenever the variant changes. Same
|
|
1193 |
+ |
// walk as `makeover-webview`'s `<optgroup>` emission, which is what
|
|
1194 |
+ |
// keeps two renderers from disagreeing about where a group starts.
|
|
1195 |
+ |
Control::Themed => {
|
|
1196 |
+ |
let value = match filling {
|
|
1197 |
+ |
Filling::Text(text) => text,
|
|
1198 |
+ |
_ => &mut discard,
|
|
1199 |
+ |
};
|
|
1200 |
+ |
let shown = themed_text(field, value);
|
|
1201 |
+ |
ComboBox::from_id_salt(field.name)
|
|
1202 |
+ |
.selected_text(RichText::new(shown).color(palette.content))
|
|
1203 |
+ |
.show_ui(ui, |ui| {
|
|
1204 |
+ |
if let Some(follow) = field.follows {
|
|
1205 |
+ |
// First and outside every heading. It names no theme
|
|
1206 |
+ |
// and sits in no variant, so a heading over it would be
|
|
1207 |
+ |
// inventing a fourth variant for one row.
|
|
1208 |
+ |
ui.selectable_value(
|
|
1209 |
+ |
value,
|
|
1210 |
+ |
follow.value.to_owned(),
|
|
1211 |
+ |
RichText::new(follow.label).color(option_color(
|
|
1212 |
+ |
value,
|
|
1213 |
+ |
follow.value,
|
|
1214 |
+ |
palette,
|
|
1215 |
+ |
)),
|
|
1216 |
+ |
);
|
|
1217 |
+ |
}
|
|
1218 |
+ |
let mut open: Option<ThemeVariant> = None;
|
|
1219 |
+ |
for theme in field.themes {
|
|
1220 |
+ |
if open != Some(theme.variant) {
|
|
1221 |
+ |
// A heading rather than a `selectable_value`: it is
|
|
1222 |
+ |
// not pickable, and egui has no inert row that
|
|
1223 |
+ |
// still reads as a row. `content_muted` is the tone
|
|
1224 |
+ |
// for something that is not an answer, which is the
|
|
1225 |
+ |
// ghost text's tone eight lines up.
|
|
1226 |
+ |
if open.is_some() {
|
|
1227 |
+ |
ui.separator();
|
|
1228 |
+ |
}
|
|
1229 |
+ |
ui.label(
|
|
1230 |
+ |
RichText::new(theme.variant.heading()).color(palette.content_muted),
|
|
1231 |
+ |
);
|
|
1232 |
+ |
open = Some(theme.variant);
|
|
1233 |
+ |
}
|
|
1234 |
+ |
ui.selectable_value(
|
|
1235 |
+ |
value,
|
|
1236 |
+ |
theme.id.to_owned(),
|
|
1237 |
+ |
RichText::new(format!("{} {}", theme.name, theme.contrast.badge()))
|
|
1238 |
+ |
.color(option_color(value, theme.id, palette)),
|
|
1239 |
+ |
);
|
|
1240 |
+ |
}
|
|
1241 |
+ |
})
|
|
1242 |
+ |
.response
|
|
1243 |
+ |
}
|
| 1175 |
1244 |
|
}
|
| 1176 |
1245 |
|
}
|
| 1177 |
1246 |
|
|
|
1247 |
+ |
/// What a theme picker's closed control reads.
|
|
1248 |
+ |
///
|
|
1249 |
+ |
/// [`chosen_text`]'s counterpart, and it is separate for the reason
|
|
1250 |
+ |
/// [`Control::Themed`] is: the label lives on a [`makeover_layout::ThemeChoice`]
|
|
1251 |
+ |
/// rather than on a [`Choice`], and the follow row is a third place to look.
|
|
1252 |
+ |
///
|
|
1253 |
+ |
/// No placeholder arm. A theme picker is never unanswered in the way a select
|
|
1254 |
+ |
/// is — an app that resolved a theme to paint this control with has one — and
|
|
1255 |
+ |
/// falling back to the raw value is the honest report on a stored id whose
|
|
1256 |
+ |
/// theme has since been deleted.
|
|
1257 |
+ |
fn themed_text<'a>(field: &'a Field<'a>, value: &'a str) -> &'a str {
|
|
1258 |
+ |
if let Some(follow) = field.follows
|
|
1259 |
+ |
&& follow.value == value
|
|
1260 |
+ |
{
|
|
1261 |
+ |
return follow.label;
|
|
1262 |
+ |
}
|
|
1263 |
+ |
field
|
|
1264 |
+ |
.themes
|
|
1265 |
+ |
.iter()
|
|
1266 |
+ |
.find(|theme| theme.id == value)
|
|
1267 |
+ |
.map_or(value, |theme| theme.name)
|
|
1268 |
+ |
}
|
|
1269 |
+ |
|
| 1178 |
1270 |
|
/// One field, as the column the app drops into its form.
|
| 1179 |
1271 |
|
///
|
| 1180 |
1272 |
|
/// The anatomy is `makeover-webview`'s, so the two renderers put a form
|
| 1641 |
1733 |
|
// What decides whether the control gets a well. A well is for what the
|
| 1642 |
1734 |
|
// user looks into, and only one of these is.
|
| 1643 |
1735 |
|
assert_eq!(control_shape(FieldKind::Select), Control::Chosen);
|
|
1736 |
+ |
// Not the wildcard. A theme picker falling to `Control::Typed` would
|
|
1737 |
+ |
// draw a text box over a resolved list, which is worse than the select
|
|
1738 |
+ |
// every app had before the member existed.
|
|
1739 |
+ |
assert_eq!(control_shape(FieldKind::Theme), Control::Themed);
|
| 1644 |
1740 |
|
assert_eq!(control_shape(FieldKind::Radio), Control::Listed);
|
| 1645 |
1741 |
|
assert_eq!(control_shape(FieldKind::Checkbox), Control::Toggled);
|
| 1646 |
1742 |
|
for k in [
|
| 1936 |
2032 |
|
assert_eq!(d.margin, Margin::ZERO);
|
| 1937 |
2033 |
|
assert!((d.stroke - 1.0).abs() < f32::EPSILON);
|
| 1938 |
2034 |
|
}
|
|
2035 |
+ |
|
|
2036 |
+ |
#[test]
|
|
2037 |
+ |
fn a_theme_picker_reads_its_chosen_theme_by_name() {
|
|
2038 |
+ |
const THEMES: &[makeover_layout::ThemeChoice<'_>] = &[makeover_layout::ThemeChoice::new(
|
|
2039 |
+ |
"carbonfox",
|
|
2040 |
+ |
"Carbonfox",
|
|
2041 |
+ |
ThemeVariant::Dark,
|
|
2042 |
+ |
makeover_layout::Contrast::High,
|
|
2043 |
+ |
)];
|
|
2044 |
+ |
let field = Field::theme("theme", "Theme", THEMES)
|
|
2045 |
+ |
.following(Choice::new("system", "Follow System"));
|
|
2046 |
+ |
|
|
2047 |
+ |
assert_eq!(themed_text(&field, "carbonfox"), "Carbonfox");
|
|
2048 |
+ |
assert_eq!(themed_text(&field, "system"), "Follow System");
|
|
2049 |
+ |
// A stored id whose theme has been deleted reads as itself rather than
|
|
2050 |
+ |
// as an empty box, which is the honest report on the config as it
|
|
2051 |
+ |
// stands.
|
|
2052 |
+ |
assert_eq!(themed_text(&field, "gone"), "gone");
|
|
2053 |
+ |
}
|
| 1939 |
2054 |
|
}
|