Add panel-wide styles and the Morph hub (#26)
Panel appearance now applies to the whole panel, with seven styles: Floating, Capsules, Notch, Edge to edge, Fused islands, Adaptive and Morph. - Share composition, geometry, and motion through the renderer-independent `ferese-panel` crate, including native materials, input regions, and Settings previews. - Add the Morph hub with media, calendar, notifications, controls, workspace previews, hover feedback, and attached mode. Keep recording and low-battery status visible and respect window clearance. - Cache workspace preview geometry, draw thumbnails directly, centralize Morph transitions, and keep rendering settings local to each shell instance. - Put Record Screen in the Morph header and show the display control only when an external monitor is connected. Update panel documentation, the desktop screenshot, and native check style names. Local validation included 660 compositor tests, 191 shell tests, isolated preview-cache checks, GPU Morph captures at 1×/1.25×/1.5×, formatting, and Clippy with warnings denied. The final header change passed all 23 Morph tests. Release workflow jobs are skipped for this PR.
This commit is contained in:
1 parent
42c3b290dd
commit
e22f0ebef9
131 files changed
+12562
-1634
No files matched your search
@@ -0,0 +1,578 @@
|
||||
use cosmic::iced::advanced::Renderer as _;
|
||||
use cosmic::iced::advanced::graphics::geometry::{Cache, Path, Renderer as _, Stroke};
|
||||
use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer, widget};
|
||||
use cosmic::iced::{Color, Event, Length, Point, Rectangle, Size, Vector};
|
||||
use cosmic::{Element, Theme};
|
||||
use ferese_panel::scene::{GroupBounds, Parameters, Scene};
|
||||
use ferese_panel::shape::{PixelGrid, ZonePosition};
|
||||
|
||||
pub struct Options {
|
||||
pub parameters: Parameters,
|
||||
pub bounds: Option<widget::Id>,
|
||||
pub groups: Vec<(widget::Id, ZonePosition)>,
|
||||
pub scale: f64,
|
||||
pub high_contrast: bool,
|
||||
pub background: Color,
|
||||
pub border: Color,
|
||||
pub divider: Color,
|
||||
pub native: bool,
|
||||
}
|
||||
|
||||
pub fn frame<'a, M: 'a>(
|
||||
content: Element<'a, M>,
|
||||
options: Options,
|
||||
present: impl Fn(&Scene) + 'a,
|
||||
changed: Option<Changed<'a, M>>,
|
||||
) -> Element<'a, M> {
|
||||
Element::new(Surface {
|
||||
content,
|
||||
options,
|
||||
present: Box::new(present),
|
||||
changed,
|
||||
})
|
||||
}
|
||||
|
||||
type Present<'a> = Box<dyn Fn(&Scene) + 'a>;
|
||||
type Changed<'a, M> = Box<dyn Fn(&Scene) -> M + 'a>;
|
||||
|
||||
struct Surface<'a, M> {
|
||||
content: Element<'a, M>,
|
||||
options: Options,
|
||||
present: Present<'a>,
|
||||
changed: Option<Changed<'a, M>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
scene: Scene,
|
||||
cache: Cache<cosmic::Renderer>,
|
||||
colors: Option<(Color, Color, Color, bool, u64, bool)>,
|
||||
published: Option<Scene>,
|
||||
}
|
||||
|
||||
struct Collect<'a> {
|
||||
targets: &'a [(widget::Id, ZonePosition)],
|
||||
origin: Point,
|
||||
groups: Vec<GroupBounds>,
|
||||
bounds_id: Option<&'a widget::Id>,
|
||||
bounds: Option<ferese_layout::Rect>,
|
||||
}
|
||||
|
||||
impl widget::Operation for Collect<'_> {
|
||||
fn traverse(&mut self, children: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
children(self);
|
||||
}
|
||||
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
|
||||
if self.bounds_id.is_some() && id == self.bounds_id {
|
||||
self.bounds = Some(ferese_layout::Rect::new(
|
||||
(bounds.x - self.origin.x).into(),
|
||||
(bounds.y - self.origin.y).into(),
|
||||
bounds.width.into(),
|
||||
bounds.height.into(),
|
||||
));
|
||||
}
|
||||
if let Some((_, zone)) = self.targets.iter().find(|(target, _)| Some(target) == id) {
|
||||
self.groups.push(GroupBounds {
|
||||
zone: *zone,
|
||||
bounds: ferese_layout::Rect::new(
|
||||
(bounds.x - self.origin.x).into(),
|
||||
(bounds.y - self.origin.y).into(),
|
||||
bounds.width.into(),
|
||||
bounds.height.into(),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Surface<'_, M> {
|
||||
fn measure(&mut self, tree: &mut widget::Tree, layout: Layout<'_>, renderer: &cosmic::Renderer) {
|
||||
let mut collector = Collect {
|
||||
targets: &self.options.groups,
|
||||
origin: layout.bounds().position(),
|
||||
groups: Vec::new(),
|
||||
bounds_id: self.options.bounds.as_ref(),
|
||||
bounds: None,
|
||||
};
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(&mut tree.children[0], layout, renderer, &mut collector);
|
||||
let size = layout.bounds().size();
|
||||
let scene = PixelGrid::new(self.options.scale)
|
||||
.and_then(|grid| {
|
||||
ferese_panel::scene::derive(
|
||||
self.options.parameters,
|
||||
collector
|
||||
.bounds
|
||||
.unwrap_or_else(|| ferese_layout::Rect::new(0., 0., size.width.into(), size.height.into())),
|
||||
&collector.groups,
|
||||
grid,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let colors = (
|
||||
self.options.background,
|
||||
self.options.border,
|
||||
self.options.divider,
|
||||
self.options.native,
|
||||
self.options.scale.to_bits(),
|
||||
self.options.high_contrast,
|
||||
);
|
||||
if state.scene != scene || state.colors != Some(colors) {
|
||||
state.scene = scene;
|
||||
state.colors = Some(colors);
|
||||
state.cache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Widget<M, Theme, cosmic::Renderer> for Surface<'_, M> {
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
widget::tree::Tag::of::<State>()
|
||||
}
|
||||
fn state(&self) -> widget::tree::State {
|
||||
widget::tree::State::new(State::default())
|
||||
}
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
vec![widget::Tree::new(&self.content)]
|
||||
}
|
||||
fn diff(&mut self, tree: &mut widget::Tree) {
|
||||
tree.diff_children(std::slice::from_mut(&mut self.content));
|
||||
}
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &cosmic::Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let node = self
|
||||
.content
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[0], renderer, limits);
|
||||
self.measure(tree, Layout::new(&node), renderer);
|
||||
node
|
||||
}
|
||||
fn a11y_nodes(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
tree: &widget::Tree,
|
||||
cursor: mouse::Cursor,
|
||||
) -> iced_accessibility::A11yTree {
|
||||
self.content.as_widget().a11y_nodes(layout, &tree.children[0], cursor)
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &cosmic::Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, M>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
if matches!(event, Event::Window(cosmic::iced::window::Event::RedrawRequested(_))) && !shell.is_layout_invalid()
|
||||
{
|
||||
self.measure(tree, layout, renderer);
|
||||
(self.present)(&tree.state.downcast_ref::<State>().scene);
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
if state.published.as_ref() != Some(&state.scene) {
|
||||
if let Some(changed) = &self.changed {
|
||||
shell.publish(changed(&state.scene));
|
||||
}
|
||||
state.published = Some(state.scene.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut cosmic::Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
if !self.options.native
|
||||
|| self.options.border.a > 0.
|
||||
|| (!state.scene.dividers.is_empty() && self.options.divider.a > 0.)
|
||||
{
|
||||
let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
|
||||
let scale = if self.options.scale.is_finite() && self.options.scale > 0. {
|
||||
self.options.scale
|
||||
} else {
|
||||
1.
|
||||
};
|
||||
let hairline = 1. / scale as f32;
|
||||
let tolerance = (0.2 / scale).clamp(0.01, 1.);
|
||||
if let Ok(contours) = ferese_panel::outline::contours(&state.scene, tolerance) {
|
||||
let path = Path::new(|builder| {
|
||||
for contour in contours {
|
||||
if let Some((first, rest)) = contour.split_first() {
|
||||
builder.move_to(Point::new(first[0] as f32, first[1] as f32));
|
||||
for point in rest {
|
||||
builder.line_to(Point::new(point[0] as f32, point[1] as f32));
|
||||
}
|
||||
builder.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
if !self.options.native {
|
||||
frame.fill(&path, self.options.background);
|
||||
}
|
||||
if self.options.border.a > 0. {
|
||||
let width = if self.options.high_contrast {
|
||||
hairline.max(1.)
|
||||
} else {
|
||||
hairline
|
||||
};
|
||||
if self.options.parameters.style == ferese_panel::PanelStyle::Flat {
|
||||
let y = match self.options.parameters.edge {
|
||||
ferese_panel::Edge::Top => bounds.height - width / 2.,
|
||||
ferese_panel::Edge::Bottom => width / 2.,
|
||||
};
|
||||
frame.stroke(
|
||||
&Path::line(Point::new(0., y), Point::new(bounds.width, y)),
|
||||
Stroke::default().with_color(self.options.border).with_width(width),
|
||||
);
|
||||
} else if self.options.parameters.style == ferese_panel::PanelStyle::Notch
|
||||
|| (self.options.parameters.style == ferese_panel::PanelStyle::Morph
|
||||
&& self.options.parameters.attached)
|
||||
{
|
||||
if let Ok(contours) = ferese_panel::outline::notch_border(
|
||||
&state.scene,
|
||||
tolerance,
|
||||
self.options.parameters.edge,
|
||||
) {
|
||||
let border = Path::new(|builder| {
|
||||
for contour in contours {
|
||||
if let Some((first, rest)) = contour.split_first() {
|
||||
builder.move_to(Point::new(first[0] as f32, first[1] as f32));
|
||||
for point in rest {
|
||||
builder.line_to(Point::new(point[0] as f32, point[1] as f32));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
frame.stroke(
|
||||
&border,
|
||||
Stroke::default().with_color(self.options.border).with_width(width),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
frame.stroke(
|
||||
&path,
|
||||
Stroke::default().with_color(self.options.border).with_width(width),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for [x1, y1, x2, y2] in &state.scene.dividers {
|
||||
frame.stroke(
|
||||
&Path::line(Point::new(*x1 as f32, *y1 as f32), Point::new(*x2 as f32, *y2 as f32)),
|
||||
Stroke::default().with_color(self.options.divider).with_width(hairline),
|
||||
);
|
||||
}
|
||||
});
|
||||
renderer.with_layer(*viewport, |renderer| {
|
||||
renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| {
|
||||
renderer.draw_geometry(geometry)
|
||||
})
|
||||
});
|
||||
}
|
||||
// Keep content after cached vector geometry in the renderer's layer order.
|
||||
// Parent-layer text can otherwise be batched underneath the background.
|
||||
renderer.with_layer(*viewport, |renderer| {
|
||||
self.content
|
||||
.as_widget()
|
||||
.draw(&tree.children[0], renderer, theme, style, layout, cursor, viewport);
|
||||
});
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &cosmic::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content
|
||||
.as_widget()
|
||||
.mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
|
||||
}
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &cosmic::Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(&mut tree.children[0], layout, renderer, operation);
|
||||
}
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
tree: &'a mut widget::Tree,
|
||||
layout: Layout<'a>,
|
||||
renderer: &cosmic::Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<cosmic::iced::advanced::overlay::Element<'a, M, Theme, cosmic::Renderer>> {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(&mut tree.children[0], layout, renderer, viewport, translation)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmic::iced::advanced::renderer::Headless;
|
||||
use cosmic::iced::{Font, Pixels};
|
||||
use ferese_panel::shape::Style;
|
||||
|
||||
#[test]
|
||||
fn border_coverage_tracks_physical_scale_and_high_contrast() {
|
||||
let mut renderer = cosmic::iced::futures::executor::block_on(<cosmic::Renderer as Headless>::new(
|
||||
Font::default(),
|
||||
Pixels(14.),
|
||||
Some("tiny-skia"),
|
||||
))
|
||||
.unwrap();
|
||||
for scale in [0.75, 1., 1.25, 1.5, 2.] {
|
||||
for high_contrast in [false, true] {
|
||||
let mut view: Element<'_, ()> = frame(
|
||||
cosmic::widget::Space::new().width(64).height(32).into(),
|
||||
Options {
|
||||
parameters: Parameters {
|
||||
style: Style::Flat,
|
||||
edge: ferese_panel::Edge::Bottom,
|
||||
..Default::default()
|
||||
},
|
||||
bounds: None,
|
||||
groups: Vec::new(),
|
||||
scale,
|
||||
high_contrast,
|
||||
background: Color::TRANSPARENT,
|
||||
border: Color::WHITE,
|
||||
divider: Color::TRANSPARENT,
|
||||
native: true,
|
||||
},
|
||||
|_| {},
|
||||
None,
|
||||
);
|
||||
let mut tree = widget::Tree::new(&view);
|
||||
let bounds = Rectangle::with_size(Size::new(64., 32.));
|
||||
let node =
|
||||
view.as_widget_mut()
|
||||
.layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, bounds.size()));
|
||||
renderer.reset(bounds);
|
||||
view.as_widget().draw(
|
||||
&tree,
|
||||
&mut renderer,
|
||||
&Theme::default(),
|
||||
&renderer::Style {
|
||||
text_color: Color::WHITE,
|
||||
icon_color: Color::WHITE,
|
||||
scale_factor: scale,
|
||||
},
|
||||
Layout::new(&node),
|
||||
mouse::Cursor::Unavailable,
|
||||
&bounds,
|
||||
);
|
||||
let size = Size::new((64. * scale) as u32, (32. * scale) as u32);
|
||||
let pixels = renderer.screenshot(size, scale as f32, Color::TRANSPARENT);
|
||||
let coverage: f64 = (0..size.height)
|
||||
.map(|y| f64::from(pixels[((y * size.width + size.width / 2) * 4 + 3) as usize]) / 255.)
|
||||
.sum();
|
||||
let expected = if high_contrast { scale.max(1.) } else { 1. };
|
||||
assert!(
|
||||
(coverage - expected).abs() < 0.03,
|
||||
"scale {scale}, high contrast {high_contrast}: {coverage} physical pixels, expected {expected}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_surface_background_keeps_parent_layer_text_visible() {
|
||||
let mut renderer = cosmic::iced::futures::executor::block_on(<cosmic::Renderer as Headless>::new(
|
||||
Font::default(),
|
||||
Pixels(14.),
|
||||
Some("tiny-skia"),
|
||||
))
|
||||
.unwrap();
|
||||
for scale in [1., 1.25, 1.5] {
|
||||
let content = cosmic::widget::container(
|
||||
cosmic::widget::text("10:09").class(cosmic::theme::Text::Color(Color::WHITE)),
|
||||
)
|
||||
.center_x(150)
|
||||
.center_y(34);
|
||||
let mut view: Element<'_, ()> = frame(
|
||||
content.into(),
|
||||
Options {
|
||||
parameters: Parameters {
|
||||
style: Style::Morph,
|
||||
height: 34.,
|
||||
..Default::default()
|
||||
},
|
||||
bounds: None,
|
||||
groups: Vec::new(),
|
||||
scale,
|
||||
high_contrast: false,
|
||||
background: Color::BLACK,
|
||||
border: Color::TRANSPARENT,
|
||||
divider: Color::TRANSPARENT,
|
||||
native: false,
|
||||
},
|
||||
|_| {},
|
||||
None,
|
||||
);
|
||||
let mut tree = widget::Tree::new(&view);
|
||||
let viewport = Rectangle::with_size(Size::new(150., 34.));
|
||||
let node =
|
||||
view.as_widget_mut()
|
||||
.layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, viewport.size()));
|
||||
renderer.reset(viewport);
|
||||
view.as_widget().draw(
|
||||
&tree,
|
||||
&mut renderer,
|
||||
&Theme::default(),
|
||||
&renderer::Style {
|
||||
text_color: Color::WHITE,
|
||||
icon_color: Color::WHITE,
|
||||
scale_factor: scale,
|
||||
},
|
||||
Layout::new(&node),
|
||||
mouse::Cursor::Unavailable,
|
||||
&viewport,
|
||||
);
|
||||
let pixels = renderer.screenshot(
|
||||
Size::new((150. * scale) as u32, (34. * scale).ceil() as u32),
|
||||
scale as f32,
|
||||
Color::BLACK,
|
||||
);
|
||||
let ink = pixels.chunks_exact(4).filter(|pixel| pixel[0] > 128).count();
|
||||
assert!(
|
||||
ink > 25,
|
||||
"clock text was covered by the surface background at scale {scale}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_surface_draws_core_outlines_at_fractional_scales() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut renderer = runtime
|
||||
.block_on(<cosmic::Renderer as Headless>::new(
|
||||
Font::default(),
|
||||
Pixels(14.),
|
||||
Some("tiny-skia"),
|
||||
))
|
||||
.unwrap();
|
||||
let theme = Theme::default();
|
||||
for style in [
|
||||
Style::Floating,
|
||||
Style::Capsules,
|
||||
Style::Notch,
|
||||
Style::Flat,
|
||||
Style::Frame,
|
||||
Style::CornerWrap,
|
||||
Style::Fused,
|
||||
] {
|
||||
for scale in [1., 1.25, 1.5] {
|
||||
let a = widget::Id::unique();
|
||||
let b = widget::Id::unique();
|
||||
let content = cosmic::widget::container(cosmic::widget::row![
|
||||
cosmic::widget::Space::new().width(4),
|
||||
cosmic::widget::container(cosmic::widget::Space::new().width(20).height(16)).id(a.clone()),
|
||||
cosmic::widget::Space::new().width(4),
|
||||
cosmic::widget::container(cosmic::widget::Space::new().width(20).height(16)).id(b.clone()),
|
||||
])
|
||||
.width(96)
|
||||
.height(64)
|
||||
.into();
|
||||
let mut view: Element<'_, ()> = frame(
|
||||
content,
|
||||
Options {
|
||||
parameters: Parameters {
|
||||
style,
|
||||
height: 16.,
|
||||
gap: 4.,
|
||||
frame_insets: [16., 4., 4., 4.],
|
||||
frame_radius: 8.,
|
||||
wrap_size: [40., 42.],
|
||||
wrap_width: 8.,
|
||||
fillet: 6.,
|
||||
..Default::default()
|
||||
},
|
||||
bounds: None,
|
||||
groups: vec![(a, ZonePosition::Start), (b, ZonePosition::Start)],
|
||||
scale,
|
||||
high_contrast: false,
|
||||
background: Color::WHITE,
|
||||
border: Color::TRANSPARENT,
|
||||
divider: Color::TRANSPARENT,
|
||||
native: false,
|
||||
},
|
||||
|_| {},
|
||||
None,
|
||||
);
|
||||
let mut tree = widget::Tree::new(&view);
|
||||
let bounds = Rectangle::new(Point::ORIGIN, Size::new(96., 64.));
|
||||
let node =
|
||||
view.as_widget_mut()
|
||||
.layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, bounds.size()));
|
||||
renderer.reset(bounds);
|
||||
view.as_widget().draw(
|
||||
&tree,
|
||||
&mut renderer,
|
||||
&theme,
|
||||
&renderer::Style {
|
||||
text_color: Color::WHITE,
|
||||
icon_color: Color::WHITE,
|
||||
scale_factor: scale,
|
||||
},
|
||||
Layout::new(&node),
|
||||
mouse::Cursor::Unavailable,
|
||||
&bounds,
|
||||
);
|
||||
let size = Size::new((96. * scale) as u32, (64. * scale) as u32);
|
||||
let pixels = Headless::screenshot(&mut renderer, size, scale as f32, Color::BLACK);
|
||||
let scene = &tree.state.downcast_ref::<State>().scene;
|
||||
assert!(!scene.paint.is_empty());
|
||||
for y in 0..size.height {
|
||||
for x in 0..size.width {
|
||||
let point = [(f64::from(x) + 0.5) / scale, (f64::from(y) + 0.5) / scale];
|
||||
let distance = scene.distance(point);
|
||||
if distance.abs() > 1. / scale {
|
||||
let ink = pixels[((y * size.width + x) * 4) as usize] > 128;
|
||||
assert_eq!(ink, distance < 0., "{style:?} scale {scale} at {point:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user