Explain runtime invariants and avoid redundant unwraps
This commit is contained in:
1 parent
34f2f07301
commit
1b51ba83b4
116 files changed
+1643
-515
No files matched your search
@@ -55,7 +55,7 @@ impl VelocityTracker {
|
||||
for col in 0..=degree {
|
||||
let pivot = (col..=degree)
|
||||
.max_by(|&a, &b| matrix[a][col].abs().total_cmp(&matrix[b][col].abs()))
|
||||
.unwrap();
|
||||
.expect("the inclusive pivot range contains at least the current column");
|
||||
matrix.swap(col, pivot);
|
||||
let divisor = matrix[col][col];
|
||||
if divisor.abs() < 1e-12 {
|
||||
|
||||
@@ -310,13 +310,18 @@ pub(crate) fn parse(node: &KdlNode) -> Result<Value, Error> {
|
||||
.into_iter()
|
||||
.filter(|rule| !names.contains(&rule.name)),
|
||||
);
|
||||
values.insert("rules".into(), serde_json::to_value(rules).unwrap());
|
||||
values.insert(
|
||||
"rules".into(),
|
||||
serde_json::to_value(rules)
|
||||
.expect("typed configuration and IPC values must serialize to JSON"),
|
||||
);
|
||||
let settings: Settings =
|
||||
serde_json::from_value(Value::Object(values)).map_err(|error| diagnostic(node, error))?;
|
||||
settings
|
||||
.validate()
|
||||
.map_err(|error| diagnostic(node, error))?;
|
||||
Ok(serde_json::to_value(settings).unwrap())
|
||||
Ok(serde_json::to_value(settings)
|
||||
.expect("typed configuration and IPC values must serialize to JSON"))
|
||||
}
|
||||
fn builtins() -> Vec<Rule> {
|
||||
[
|
||||
|
||||
@@ -166,7 +166,10 @@ fn object(doc: &KdlDocument, parent: &str) -> Result<Map<String, Value>, Error>
|
||||
let records = result
|
||||
.entry(key)
|
||||
.or_insert_with(|| Value::Array(Vec::new()));
|
||||
records.as_array_mut().unwrap().push(value);
|
||||
records
|
||||
.as_array_mut()
|
||||
.expect("repeated KDL records are stored as arrays")
|
||||
.push(value);
|
||||
} else {
|
||||
insert(&mut result, key, value)?;
|
||||
}
|
||||
@@ -255,7 +258,10 @@ pub(crate) fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result<Valu
|
||||
return Err(Error(format!("{} requires one value", node.name())));
|
||||
}
|
||||
|
||||
Ok(args.into_iter().next().unwrap())
|
||||
Ok(args
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("a scalar KDL node was checked to have one argument"))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -305,7 +311,8 @@ impl Document {
|
||||
let mut document = self.clone();
|
||||
document.value["theme"] =
|
||||
serde_json::to_value(&theme.tokens).expect("serializable theme tokens");
|
||||
document.value["theme"]["appearance"] = serde_json::to_value(theme.appearance).unwrap();
|
||||
document.value["theme"]["appearance"] = serde_json::to_value(theme.appearance)
|
||||
.expect("typed configuration and IPC values must serialize to JSON");
|
||||
document
|
||||
}
|
||||
|
||||
@@ -882,7 +889,10 @@ fn kdl_value(value: &Value) -> Result<KdlValue, Error> {
|
||||
} else if let Some(i) = n.as_u64() {
|
||||
KdlValue::Integer(i.into())
|
||||
} else {
|
||||
KdlValue::Float(n.as_f64().unwrap())
|
||||
KdlValue::Float(
|
||||
n.as_f64()
|
||||
.expect("JSON numbers fit the KDL numeric representation"),
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => return Err(Error("Expected a scalar value".into())),
|
||||
|
||||
@@ -64,10 +64,12 @@ pub fn builtins() -> Vec<Family> {
|
||||
id: (*id).into(),
|
||||
name: (*name).into(),
|
||||
light: Some(Palette::from(
|
||||
&crate::theme::preset(light, Appearance::Light).unwrap(),
|
||||
&crate::theme::preset(light, Appearance::Light)
|
||||
.expect("built-in theme families must reference registered presets"),
|
||||
)),
|
||||
dark: Some(Palette::from(
|
||||
&crate::theme::preset(dark, Appearance::Dark).unwrap(),
|
||||
&crate::theme::preset(dark, Appearance::Dark)
|
||||
.expect("built-in theme families must reference registered presets"),
|
||||
)),
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -552,8 +552,10 @@ fn token_value(tokens: &Tokens) -> Result<Value, String> {
|
||||
let mut value = serde_json::to_value(tokens).map_err(|error| error.to_string())?;
|
||||
// Keep authoring defaults available for merging. The wire format omits
|
||||
// automatic styles so older snapshots still round-trip unchanged.
|
||||
value["focus_ring"]["style"] = serde_json::to_value(tokens.focus_ring.style).unwrap();
|
||||
value["border"]["style"] = serde_json::to_value(tokens.border.style).unwrap();
|
||||
value["focus_ring"]["style"] = serde_json::to_value(tokens.focus_ring.style)
|
||||
.expect("typed configuration and IPC values must serialize to JSON");
|
||||
value["border"]["style"] = serde_json::to_value(tokens.border.style)
|
||||
.expect("typed configuration and IPC values must serialize to JSON");
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
@@ -673,8 +675,16 @@ pub fn preset(name: &str, appearance: Appearance) -> Result<Tokens, String> {
|
||||
} else {
|
||||
[1.; 4]
|
||||
};
|
||||
t.colors.surface_raised = hex(blend(rgba(base).unwrap(), shade, 0.055));
|
||||
t.colors.application_background = hex(blend(rgba(base).unwrap(), shade, 0.025));
|
||||
t.colors.surface_raised = hex(blend(
|
||||
rgba(base).expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
shade,
|
||||
0.055,
|
||||
));
|
||||
t.colors.application_background = hex(blend(
|
||||
rgba(base).expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
shade,
|
||||
0.025,
|
||||
));
|
||||
t.colors.text_primary = text.into();
|
||||
t.colors.text_muted = muted.into();
|
||||
t.colors.accent = accent.into();
|
||||
@@ -708,8 +718,10 @@ pub fn authored_warnings(tokens: &Tokens) -> Vec<String> {
|
||||
),
|
||||
];
|
||||
let check = |foreground: &str, background: &str, minimum: f64| {
|
||||
let foreground = rgba(foreground).unwrap();
|
||||
let background = rgba(background).unwrap();
|
||||
let foreground =
|
||||
rgba(foreground).expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let background =
|
||||
rgba(background).expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
contrast(composite(foreground, background), background) >= minimum
|
||||
};
|
||||
for (name, background) in surfaces {
|
||||
@@ -813,7 +825,8 @@ fn transform(t: &mut Tokens, accessibility: &Accessibility, warnings: &mut Vec<S
|
||||
for target in [[0., 0., 0., 1.], [1.; 4]] {
|
||||
for step in 0..=1000 {
|
||||
let distance = f64::from(step) / 1000.;
|
||||
let color = rgba(&hex(blend(requested, target, distance))).unwrap();
|
||||
let color = rgba(&hex(blend(requested, target, distance)))
|
||||
.expect("hex output must be a valid RGBA color");
|
||||
if acceptable(color) && distance < best_distance {
|
||||
best = Some(color);
|
||||
best_distance = distance;
|
||||
@@ -858,14 +871,17 @@ fn transform(t: &mut Tokens, accessibility: &Accessibility, warnings: &mut Vec<S
|
||||
}
|
||||
if accessibility.increase_contrast {
|
||||
t.colors.border = hex(readable_across(
|
||||
rgba(&t.colors.border).unwrap(),
|
||||
rgba(&t.colors.border).expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
&backgrounds,
|
||||
3.,
|
||||
));
|
||||
if backgrounds
|
||||
.iter()
|
||||
.any(|background| contrast(rgba(&t.colors.border).unwrap(), *background) < 3.)
|
||||
{
|
||||
if backgrounds.iter().any(|background| {
|
||||
contrast(
|
||||
rgba(&t.colors.border)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
*background,
|
||||
) < 3.
|
||||
}) {
|
||||
warnings.push("Border cannot reach 3:1 contrast on all configured surfaces".into());
|
||||
}
|
||||
resolve_material_contrast(t, minimum);
|
||||
@@ -889,10 +905,14 @@ fn resolve_gradients(tokens: &mut Tokens, preset_end: Option<&str>) {
|
||||
};
|
||||
|
||||
let backgrounds = [
|
||||
rgba(&tokens.colors.surface_base).unwrap(),
|
||||
rgba(&tokens.colors.surface_raised).unwrap(),
|
||||
rgba(&tokens.colors.application_background).unwrap(),
|
||||
rgba(&tokens.surface.bar.background).unwrap(),
|
||||
rgba(&tokens.colors.surface_base)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
rgba(&tokens.colors.surface_raised)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
rgba(&tokens.colors.application_background)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
rgba(&tokens.surface.bar.background)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
];
|
||||
for (paint, color, endpoint, focus) in [
|
||||
(
|
||||
@@ -915,7 +935,11 @@ fn resolve_gradients(tokens: &mut Tokens, preset_end: Option<&str>) {
|
||||
|
||||
let to = endpoint.map_or_else(|| shade(color), str::to_owned);
|
||||
let to = if focus {
|
||||
hex(readable_across(rgba(&to).unwrap(), &backgrounds, 3.))
|
||||
hex(readable_across(
|
||||
rgba(&to).expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
&backgrounds,
|
||||
3.,
|
||||
))
|
||||
} else {
|
||||
to
|
||||
};
|
||||
@@ -931,10 +955,14 @@ fn resolve_material_contrast(tokens: &mut Tokens, minimum: f64) {
|
||||
if tokens.material.style == "solid" {
|
||||
return;
|
||||
}
|
||||
let surface = rgba(&tokens.colors.surface_base).unwrap();
|
||||
let bar = rgba(&tokens.surface.bar.background).unwrap();
|
||||
let raised = rgba(&tokens.colors.surface_raised).unwrap();
|
||||
let app = rgba(&tokens.colors.application_background).unwrap();
|
||||
let surface = rgba(&tokens.colors.surface_base)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let bar = rgba(&tokens.surface.bar.background)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let raised = rgba(&tokens.colors.surface_raised)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let app = rgba(&tokens.colors.application_background)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let initial = tokens.material.opacity * tokens.material.tint_strength;
|
||||
let readable = |backgrounds: &[[f64; 4]]| {
|
||||
[[0., 0., 0., 1.], [1.; 4]].into_iter().any(|text| {
|
||||
@@ -962,7 +990,11 @@ fn resolve_material_contrast(tokens: &mut Tokens, minimum: f64) {
|
||||
(&mut tokens.surface.bar.text_primary, bar.as_slice()),
|
||||
(&mut tokens.surface.bar.text_muted, bar.as_slice()),
|
||||
] {
|
||||
*color = hex(readable_across(rgba(color).unwrap(), backgrounds, minimum));
|
||||
*color = hex(readable_across(
|
||||
rgba(color).expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
backgrounds,
|
||||
minimum,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -184,7 +184,15 @@ impl OutputWorkspaceMap {
|
||||
let mut removed = self
|
||||
.connected_outputs()
|
||||
.filter(|id| !desired.contains_key(id))
|
||||
.map(|id| (id, outputs.outputs.remove(&id).unwrap()))
|
||||
.map(|id| {
|
||||
(
|
||||
id,
|
||||
outputs
|
||||
.outputs
|
||||
.remove(&id)
|
||||
.expect("workspace reconciliation only accesses connected outputs"),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Preserve the original focused workspace when several outputs vanish.
|
||||
removed.sort_by_key(|(id, _)| (Some(*id) == self.focused, *id));
|
||||
@@ -196,7 +204,10 @@ impl OutputWorkspaceMap {
|
||||
// against the final inventory, where that donor no longer needs it.
|
||||
for (id, record) in returning {
|
||||
let reclaimed = outputs.reclaim(id, &record);
|
||||
let state = outputs.outputs.get_mut(&id).unwrap();
|
||||
let state = outputs
|
||||
.outputs
|
||||
.get_mut(&id)
|
||||
.expect("workspace reconciliation only accesses connected outputs");
|
||||
state.workspaces.extend(reclaimed.iter().copied());
|
||||
if reclaimed.contains(&record.active) {
|
||||
state.activate(record.active);
|
||||
@@ -635,7 +646,7 @@ impl OutputWorkspaceMap {
|
||||
self.homes.insert(workspace, output);
|
||||
self.outputs
|
||||
.get_mut(&output)
|
||||
.unwrap()
|
||||
.expect("workspace reconciliation only accesses connected outputs")
|
||||
.workspaces
|
||||
.insert(workspace);
|
||||
self.bump_revision(workspace);
|
||||
|
||||
@@ -544,7 +544,10 @@ impl WorkspaceSet {
|
||||
replacements.push((workspace.id, layout));
|
||||
}
|
||||
for (id, layout) in replacements {
|
||||
self.workspaces.get_mut(&id).unwrap().layout = layout;
|
||||
self.workspaces
|
||||
.get_mut(&id)
|
||||
.expect("replacement layouts were built from existing workspace IDs")
|
||||
.layout = layout;
|
||||
}
|
||||
self.default_layout_mode = mode;
|
||||
self.default_column_width = width;
|
||||
|
||||
@@ -41,7 +41,10 @@ impl OutputWorkspaceMap {
|
||||
id,
|
||||
output,
|
||||
index: index as u32 + 1,
|
||||
window_count: workspaces.workspace(id).unwrap().window_count(),
|
||||
window_count: workspaces
|
||||
.workspace(id)
|
||||
.expect("workspace views enumerate registered workspace IDs")
|
||||
.window_count(),
|
||||
visible,
|
||||
focused: visible && self.focused_output() == Some(output),
|
||||
}
|
||||
|
||||
@@ -666,7 +666,10 @@ impl ScrollingLayout {
|
||||
let viewport_center = bounds.x + bounds.width / 2.0;
|
||||
let window_center = rect.x + rect.width / 2.0;
|
||||
self.viewport_x += window_center - viewport_center;
|
||||
let active = self.window_location(window).unwrap().0;
|
||||
let active = self
|
||||
.window_location(window)
|
||||
.expect("geometry resolution confirmed the window belongs to a column")
|
||||
.0;
|
||||
let inner = finite_nonnegative(gaps.inner);
|
||||
let positions = self
|
||||
.columns
|
||||
@@ -678,7 +681,10 @@ impl ScrollingLayout {
|
||||
Some(position)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let viewport_width = self.last_column_sizing.unwrap().viewport_width;
|
||||
let viewport_width = self
|
||||
.last_column_sizing
|
||||
.expect("geometry resolution records the column sizing before centering")
|
||||
.viewport_width;
|
||||
self.record_viewport_basis(
|
||||
&positions,
|
||||
inner,
|
||||
|
||||
@@ -49,9 +49,9 @@ impl Geometry {
|
||||
fn from_values(values: [f64; COUNT]) -> Self {
|
||||
Self {
|
||||
bounds: Rect::new(values[0], values[1], values[2], values[3]),
|
||||
radii: values[4..8].try_into().unwrap(),
|
||||
margin: values[8..12].try_into().unwrap(),
|
||||
thickness: values[12..16].try_into().unwrap(),
|
||||
radii: [values[4], values[5], values[6], values[7]],
|
||||
margin: [values[8], values[9], values[10], values[11]],
|
||||
thickness: [values[12], values[13], values[14], values[15]],
|
||||
gap: values[16],
|
||||
fusion: values[17],
|
||||
}
|
||||
|
||||
@@ -211,10 +211,14 @@ pub fn notch_border(
|
||||
let mut paths = Vec::new();
|
||||
while let Some((a, b)) = segments.pop() {
|
||||
let mut path = vec![a, b];
|
||||
while let Some(index) = segments
|
||||
.iter()
|
||||
.position(|&(a, _)| close(a, *path.last().unwrap()))
|
||||
{
|
||||
while let Some(index) = segments.iter().position(|&(a, _)| {
|
||||
close(
|
||||
a,
|
||||
*path
|
||||
.last()
|
||||
.expect("an outline path starts with two points and only grows"),
|
||||
)
|
||||
}) {
|
||||
path.push(segments.swap_remove(index).1);
|
||||
}
|
||||
while let Some(index) = segments.iter().position(|&(_, b)| close(b, path[0])) {
|
||||
|
||||
@@ -183,10 +183,10 @@ pub fn resolve_with_gap(
|
||||
result.minimum_width = minimum.required_width();
|
||||
// Flexible content yields space without changing the stored item preference.
|
||||
for (_, item) in &candidates {
|
||||
let Some(measured) = measurements
|
||||
.get(&item.id)
|
||||
.filter(|measured| measured.minimum.is_some())
|
||||
else {
|
||||
let Some(measured) = measurements.get(&item.id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(minimum) = measured.minimum else {
|
||||
continue;
|
||||
};
|
||||
if fits(&result) {
|
||||
@@ -199,7 +199,7 @@ pub fn resolve_with_gap(
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let minimum = measured.minimum.unwrap().min(width);
|
||||
let minimum = minimum.min(width);
|
||||
result.items.insert(
|
||||
measured.id.clone(),
|
||||
Placement::Visible {
|
||||
@@ -352,10 +352,14 @@ pub fn resolve_with_gap(
|
||||
let mut high = maximum;
|
||||
for _ in 0..20 {
|
||||
let width = (low + high) * 0.5;
|
||||
*result.items.get_mut(&measured.id).unwrap() = Placement::Visible {
|
||||
representation,
|
||||
width,
|
||||
};
|
||||
*result
|
||||
.items
|
||||
.get_mut(&measured.id)
|
||||
.expect("visible items retain their allocation during width refinement") =
|
||||
Placement::Visible {
|
||||
representation,
|
||||
width,
|
||||
};
|
||||
result.update_widths(panel, overflow_width);
|
||||
if fits(&result) {
|
||||
low = width
|
||||
@@ -363,14 +367,21 @@ pub fn resolve_with_gap(
|
||||
high = width
|
||||
}
|
||||
}
|
||||
*result.items.get_mut(&measured.id).unwrap() = Placement::Visible {
|
||||
representation,
|
||||
width: low,
|
||||
};
|
||||
*result
|
||||
.items
|
||||
.get_mut(&measured.id)
|
||||
.expect("visible items retain their allocation during width refinement") =
|
||||
Placement::Visible {
|
||||
representation,
|
||||
width: low,
|
||||
};
|
||||
}
|
||||
result
|
||||
.overflow
|
||||
.sort_by_key(|id| ordered.iter().position(|item| &item.id == id).unwrap());
|
||||
result.overflow.sort_by_key(|id| {
|
||||
ordered
|
||||
.iter()
|
||||
.position(|item| &item.id == id)
|
||||
.expect("overflow contains only authored item IDs")
|
||||
});
|
||||
result.update_widths(panel, overflow_width);
|
||||
result
|
||||
}
|
||||
|
||||
@@ -108,7 +108,8 @@ pub fn readable_across(
|
||||
white
|
||||
};
|
||||
for step in 1..=100 {
|
||||
let color = rgba(&hex(blend(preferred, target, f64::from(step) / 100.))).unwrap();
|
||||
let color = rgba(&hex(blend(preferred, target, f64::from(step) / 100.)))
|
||||
.expect("hex output must be a valid RGBA color");
|
||||
if score(color) >= minimum {
|
||||
return color;
|
||||
}
|
||||
@@ -137,9 +138,12 @@ fn material_foreground(preferred: [f64; 4], backgrounds: [[f64; 4]; 2]) -> [f64;
|
||||
let outside = luminance(result) < luminance(backgrounds[0])
|
||||
|| luminance(result) > luminance(backgrounds[1]);
|
||||
if outside
|
||||
&& backgrounds
|
||||
.iter()
|
||||
.all(|background| contrast(rgba(&hex(result)).unwrap(), *background) >= 4.5)
|
||||
&& backgrounds.iter().all(|background| {
|
||||
contrast(
|
||||
rgba(&hex(result)).expect("hex output must be a valid RGBA color"),
|
||||
*background,
|
||||
) >= 4.5
|
||||
})
|
||||
{
|
||||
result
|
||||
} else {
|
||||
@@ -161,8 +165,8 @@ fn transition_paint(from: &Paint, to: &Paint, from_solid: &str, to_solid: &str,
|
||||
let (a, b) = endpoints(from, from_solid);
|
||||
let (c, d) = endpoints(to, to_solid);
|
||||
let color = |a: &str, b: &str| {
|
||||
let mut a = rgba(a).unwrap();
|
||||
let mut b = rgba(b).unwrap();
|
||||
let mut a = rgba(a).expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let mut b = rgba(b).expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
for channel in 0..3 {
|
||||
a[channel] *= a[3];
|
||||
b[channel] *= b[3];
|
||||
@@ -180,13 +184,13 @@ fn transition_paint(from: &Paint, to: &Paint, from_solid: &str, to_solid: &str,
|
||||
let from_angle = from
|
||||
.effective_gradient()
|
||||
.or(to.effective_gradient())
|
||||
.unwrap()
|
||||
.expect("at least one transition endpoint has a gradient")
|
||||
.angle
|
||||
.rem_euclid(360.);
|
||||
let to_angle = to
|
||||
.effective_gradient()
|
||||
.or(from.effective_gradient())
|
||||
.unwrap()
|
||||
.expect("at least one transition endpoint has a gradient")
|
||||
.angle
|
||||
.rem_euclid(360.);
|
||||
let delta = (to_angle - from_angle + 180.).rem_euclid(360.) - 180.;
|
||||
@@ -202,6 +206,10 @@ fn transition_paint(from: &Paint, to: &Paint, from_solid: &str, to_solid: &str,
|
||||
|
||||
impl ResolvedTheme {
|
||||
/// Bound contrast across all backdrops instead of interpolating foregrounds.
|
||||
///
|
||||
/// # Panics
|
||||
/// Both themes must contain valid hexadecimal colors and gradient endpoints,
|
||||
/// as supplied by configuration resolution. Invalid colors panic during blending.
|
||||
pub fn transition(&self, to: &Self, progress: f64) -> Self {
|
||||
if to.reduced_motion || self.accessibility != to.accessibility || progress >= 1.0 {
|
||||
return to.clone();
|
||||
@@ -212,7 +220,13 @@ impl ResolvedTheme {
|
||||
let p = progress.clamp(0., 1.);
|
||||
let p = p * p * (3. - 2. * p);
|
||||
let mut frame = to.clone();
|
||||
let mix = |a: &str, b: &str| hex(blend(rgba(a).unwrap(), rgba(b).unwrap(), p));
|
||||
let mix = |a: &str, b: &str| {
|
||||
hex(blend(
|
||||
rgba(a).expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
rgba(b).expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
p,
|
||||
))
|
||||
};
|
||||
frame.tokens.colors.surface_base = mix(
|
||||
&self.tokens.colors.surface_base,
|
||||
&to.tokens.colors.surface_base,
|
||||
@@ -256,9 +270,12 @@ impl ResolvedTheme {
|
||||
frame.appearance = preferred.appearance;
|
||||
if self.appearance == to.appearance {
|
||||
let body = [
|
||||
rgba(&frame.tokens.colors.surface_base).unwrap(),
|
||||
rgba(&frame.tokens.colors.surface_raised).unwrap(),
|
||||
rgba(&frame.tokens.colors.application_background).unwrap(),
|
||||
rgba(&frame.tokens.colors.surface_base)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
rgba(&frame.tokens.colors.surface_raised)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
rgba(&frame.tokens.colors.application_background)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
];
|
||||
let minimum = if to.accessibility.increase_contrast {
|
||||
7.0
|
||||
@@ -266,36 +283,46 @@ impl ResolvedTheme {
|
||||
4.5
|
||||
};
|
||||
frame.tokens.colors.text_primary = hex(readable_across(
|
||||
rgba(&preferred.tokens.colors.text_primary).unwrap(),
|
||||
rgba(&preferred.tokens.colors.text_primary)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
&body,
|
||||
minimum,
|
||||
));
|
||||
frame.tokens.colors.text_muted = hex(readable_across(
|
||||
rgba(&preferred.tokens.colors.text_muted).unwrap(),
|
||||
rgba(&preferred.tokens.colors.text_muted)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
&body,
|
||||
minimum,
|
||||
));
|
||||
let bar = rgba(&frame.tokens.surface.bar.background).unwrap();
|
||||
let bar = rgba(&frame.tokens.surface.bar.background)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
frame.tokens.surface.bar.text_primary = hex(readable(
|
||||
rgba(&preferred.tokens.surface.bar.text_primary).unwrap(),
|
||||
rgba(&preferred.tokens.surface.bar.text_primary)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
bar,
|
||||
minimum,
|
||||
));
|
||||
frame.tokens.surface.bar.text_muted = hex(readable(
|
||||
rgba(&preferred.tokens.surface.bar.text_muted).unwrap(),
|
||||
rgba(&preferred.tokens.surface.bar.text_muted)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
bar,
|
||||
minimum,
|
||||
));
|
||||
frame.tokens.colors.on_accent = hex(readable(
|
||||
rgba(&preferred.tokens.colors.on_accent).unwrap(),
|
||||
rgba(&frame.tokens.colors.accent).unwrap(),
|
||||
rgba(&preferred.tokens.colors.on_accent)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
rgba(&frame.tokens.colors.accent)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
4.5,
|
||||
));
|
||||
return frame;
|
||||
}
|
||||
let base = rgba(&frame.tokens.colors.surface_base).unwrap();
|
||||
let raised = rgba(&frame.tokens.colors.surface_raised).unwrap();
|
||||
let app = rgba(&frame.tokens.colors.application_background).unwrap();
|
||||
let base = rgba(&frame.tokens.colors.surface_base)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let raised = rgba(&frame.tokens.colors.surface_raised)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let app = rgba(&frame.tokens.colors.application_background)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let minimum = [base, raised, app]
|
||||
.into_iter()
|
||||
.map(luminance)
|
||||
@@ -308,8 +335,10 @@ impl ResolvedTheme {
|
||||
frame.tokens.colors.surface_raised = frame.tokens.colors.surface_base.clone();
|
||||
frame.tokens.colors.application_background = frame.tokens.colors.surface_base.clone();
|
||||
}
|
||||
let surface = rgba(&frame.tokens.colors.surface_base).unwrap();
|
||||
let bar = rgba(&frame.tokens.surface.bar.background).unwrap();
|
||||
let surface = rgba(&frame.tokens.colors.surface_base)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let bar = rgba(&frame.tokens.surface.bar.background)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let mut opacity = if frame.tokens.material.style == "solid" {
|
||||
1.
|
||||
} else {
|
||||
@@ -318,8 +347,10 @@ impl ResolvedTheme {
|
||||
for step in 0..=100 {
|
||||
let candidate = opacity + (1. - opacity) * f64::from(step) / 100.;
|
||||
let backgrounds = material_bounds(surface, candidate);
|
||||
let raised = rgba(&frame.tokens.colors.surface_raised).unwrap();
|
||||
let app = rgba(&frame.tokens.colors.application_background).unwrap();
|
||||
let raised = rgba(&frame.tokens.colors.surface_raised)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let app = rgba(&frame.tokens.colors.application_background)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let all = [backgrounds[0], backgrounds[1], raised, app];
|
||||
let minimum = all.into_iter().map(luminance).fold(f64::INFINITY, f64::min);
|
||||
let maximum = all.into_iter().map(luminance).fold(0., f64::max);
|
||||
@@ -339,38 +370,46 @@ impl ResolvedTheme {
|
||||
}
|
||||
let surface = material_bounds(surface, opacity);
|
||||
let bar = material_bounds(bar, opacity);
|
||||
let raised = rgba(&frame.tokens.colors.surface_raised).unwrap();
|
||||
let app = rgba(&frame.tokens.colors.application_background).unwrap();
|
||||
let raised = rgba(&frame.tokens.colors.surface_raised)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let app = rgba(&frame.tokens.colors.application_background)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA");
|
||||
let body_backgrounds = [surface[0], surface[1], raised, app];
|
||||
let body_bounds = [
|
||||
*body_backgrounds
|
||||
.iter()
|
||||
.min_by(|a, b| luminance(**a).total_cmp(&luminance(**b)))
|
||||
.unwrap(),
|
||||
.expect("material contrast checks contain four background samples"),
|
||||
*body_backgrounds
|
||||
.iter()
|
||||
.max_by(|a, b| luminance(**a).total_cmp(&luminance(**b)))
|
||||
.unwrap(),
|
||||
.expect("material contrast checks contain four background samples"),
|
||||
];
|
||||
frame.tokens.colors.text_primary = hex(material_foreground(
|
||||
rgba(&preferred.tokens.colors.text_primary).unwrap(),
|
||||
rgba(&preferred.tokens.colors.text_primary)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
body_bounds,
|
||||
));
|
||||
frame.tokens.colors.text_muted = hex(material_foreground(
|
||||
rgba(&preferred.tokens.colors.text_muted).unwrap(),
|
||||
rgba(&preferred.tokens.colors.text_muted)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
body_bounds,
|
||||
));
|
||||
frame.tokens.surface.bar.text_primary = hex(material_foreground(
|
||||
rgba(&preferred.tokens.surface.bar.text_primary).unwrap(),
|
||||
rgba(&preferred.tokens.surface.bar.text_primary)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
bar,
|
||||
));
|
||||
frame.tokens.surface.bar.text_muted = hex(material_foreground(
|
||||
rgba(&preferred.tokens.surface.bar.text_muted).unwrap(),
|
||||
rgba(&preferred.tokens.surface.bar.text_muted)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
bar,
|
||||
));
|
||||
frame.tokens.colors.on_accent = hex(readable(
|
||||
rgba(&preferred.tokens.colors.on_accent).unwrap(),
|
||||
rgba(&frame.tokens.colors.accent).unwrap(),
|
||||
rgba(&preferred.tokens.colors.on_accent)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
rgba(&frame.tokens.colors.accent)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
4.5,
|
||||
));
|
||||
frame
|
||||
|
||||
@@ -17,6 +17,10 @@ pub struct TileOptions {
|
||||
pub id: Id,
|
||||
}
|
||||
|
||||
/// Shows a theme family using its validated appearance palettes.
|
||||
///
|
||||
/// # Panics
|
||||
/// The family must contain a usable palette with valid hexadecimal colors.
|
||||
pub fn tile<M: Clone + 'static>(
|
||||
family: &Family,
|
||||
options: TileOptions,
|
||||
@@ -36,7 +40,9 @@ pub fn tile<M: Clone + 'static>(
|
||||
let label_color = family
|
||||
.palette(variant.unwrap_or(Appearance::Light))
|
||||
.or(family.dark.as_ref())
|
||||
.map(|palette| crate::parse_color(&palette.text).unwrap())
|
||||
.map(|palette| {
|
||||
crate::parse_color(&palette.text).expect("theme family text is a validated color")
|
||||
})
|
||||
.unwrap_or(palette.text);
|
||||
let mut caption = row([])
|
||||
.spacing(6)
|
||||
@@ -141,7 +147,9 @@ fn preview_with_radius(
|
||||
std::sync::Mutex<std::collections::HashMap<String, (Family, icon::Handle)>>,
|
||||
> = std::sync::OnceLock::new();
|
||||
let cache = CACHE.get_or_init(Default::default);
|
||||
let mut cache = cache.lock().unwrap();
|
||||
let mut cache = cache
|
||||
.lock()
|
||||
.expect("theme preview cache must not be used after a mutator panics");
|
||||
if cache.get(&key).is_none_or(|(cached, _)| cached != family) {
|
||||
let handle = icon::from_svg_bytes(
|
||||
svg_with_radius(family, variant, active, selected, radius).into_bytes(),
|
||||
@@ -207,7 +215,7 @@ fn svg_with_radius(
|
||||
result.push_str("<clipPath id=\"preview\"><polygon points=\"");
|
||||
for [x, y] in points {
|
||||
use std::fmt::Write as _;
|
||||
write!(result, "{x:.4},{y:.4} ").unwrap();
|
||||
write!(result, "{x:.4},{y:.4} ").expect("formatting into an owned String cannot fail");
|
||||
}
|
||||
result.push_str("\"/></clipPath></defs><g clip-path=\"url(#preview)\">");
|
||||
for appearance in [Appearance::Light, Appearance::Dark] {
|
||||
@@ -263,10 +271,11 @@ fn svg_with_radius(
|
||||
.palette(active.unwrap_or(Appearance::Dark))
|
||||
.or(family.dark.as_ref())
|
||||
.or(family.light.as_ref())
|
||||
.unwrap();
|
||||
.expect("a theme family provides at least one appearance palette");
|
||||
let foreground = ferese_theme_model::hex(ferese_theme_model::readable(
|
||||
[1.; 4],
|
||||
ferese_theme_model::rgba(&p.accent).unwrap(),
|
||||
ferese_theme_model::rgba(&p.accent)
|
||||
.expect("resolved theme colors must be valid hexadecimal RGBA"),
|
||||
4.5,
|
||||
));
|
||||
result.push_str(&format!(r#"<circle cx="183" cy="16" r="10" fill="{}"/><path d="M178 16l3 3 6-6" fill="none" stroke="{foreground}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>"#, p.accent));
|
||||
|
||||
@@ -136,7 +136,10 @@ impl TintCache {
|
||||
.iter()
|
||||
.position(|entry| std::ptr::eq(entry.source, source) && entry.colors == colors)
|
||||
{
|
||||
let entry = self.entries.remove(index).unwrap();
|
||||
let entry = self
|
||||
.entries
|
||||
.remove(index)
|
||||
.expect("the cache lookup identifies an existing icon entry");
|
||||
let handle = entry.handle.clone();
|
||||
self.entries.push_back(entry);
|
||||
return handle;
|
||||
@@ -147,7 +150,11 @@ impl TintCache {
|
||||
if bytes <= TINT_CACHE_BYTES {
|
||||
while self.entries.len() >= TINT_CACHE_ENTRIES || self.bytes + bytes > TINT_CACHE_BYTES
|
||||
{
|
||||
self.bytes -= self.entries.pop_front().unwrap().bytes;
|
||||
self.bytes -= self
|
||||
.entries
|
||||
.pop_front()
|
||||
.expect("cache eviction requires an entry while over budget")
|
||||
.bytes;
|
||||
}
|
||||
self.bytes += bytes;
|
||||
self.entries.push_back(TintedIcon {
|
||||
|
||||
@@ -94,8 +94,18 @@ impl Palette {
|
||||
stop.color.a = opacity;
|
||||
}
|
||||
|
||||
let from = crate::composite(candidate.stops[0].unwrap().color, self.sidebar);
|
||||
let to = crate::composite(candidate.stops[1].unwrap().color, self.sidebar);
|
||||
let from = crate::composite(
|
||||
candidate.stops[0]
|
||||
.expect("the constructed linear gradient contains both endpoint stops")
|
||||
.color,
|
||||
self.sidebar,
|
||||
);
|
||||
let to = crate::composite(
|
||||
candidate.stops[1]
|
||||
.expect("the constructed linear gradient contains both endpoint stops")
|
||||
.color,
|
||||
self.sidebar,
|
||||
);
|
||||
let lower = Color::from_rgb(from.r.min(to.r), from.g.min(to.g), from.b.min(to.b));
|
||||
let upper = Color::from_rgb(from.r.max(to.r), from.g.max(to.g), from.b.max(to.b));
|
||||
let lightness = crate::luminance(foreground);
|
||||
|
||||
@@ -6,7 +6,10 @@ pub fn font(family: Option<&str>) -> cosmic::font::Font {
|
||||
return cosmic::font::default();
|
||||
};
|
||||
static FONTS: OnceLock<Mutex<HashMap<String, cosmic::font::Font>>> = OnceLock::new();
|
||||
let mut fonts = FONTS.get_or_init(Default::default).lock().unwrap();
|
||||
let mut fonts = FONTS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.expect("shared font cache must not be used after a mutator panics");
|
||||
if let Some(font) = fonts.get(family) {
|
||||
return *font;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user