Support images with rounded corners (#3257)

* Add `Rect::ZERO`

* Add `Rounding::ZERO`

* Add `RectShape::new`

* Add `Image::rounding` to support images with rounded corners
This commit is contained in:
Emil Ernerfeldt 2023-08-15 09:29:30 +02:00 committed by GitHub
parent 481f44828c
commit 3c4223c6b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 302 additions and 123 deletions

View File

@ -555,13 +555,12 @@ impl CollapsingHeader {
let visuals = ui.style().interact_selectable(&header_response, selected);
if ui.visuals().collapsing_header_frame || show_background {
ui.painter().add(epaint::RectShape {
rect: header_response.rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke,
// stroke: Default::default(),
});
ui.painter().add(epaint::RectShape::new(
header_response.rect.expand(visuals.expansion),
visuals.rounding,
visuals.weak_bg_fill,
visuals.bg_stroke,
));
}
if selected || selectable && (header_response.hovered() || header_response.has_focus())

View File

@ -383,12 +383,12 @@ fn button_frame(
ui.painter().set(
where_to_put_background,
epaint::RectShape {
rect: outer_rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke,
},
epaint::RectShape::new(
outer_rect.expand(visuals.expansion),
visuals.rounding,
visuals.weak_bg_fill,
visuals.bg_stroke,
),
);
}

View File

@ -235,12 +235,7 @@ impl Frame {
stroke,
} = *self;
let frame_shape = Shape::Rect(epaint::RectShape {
rect: outer_rect,
rounding,
fill,
stroke,
});
let frame_shape = Shape::Rect(epaint::RectShape::new(outer_rect, rounding, fill, stroke));
if shadow == Default::default() {
frame_shape

View File

@ -311,12 +311,7 @@ impl Painter {
fill_color: impl Into<Color32>,
stroke: impl Into<Stroke>,
) {
self.add(RectShape {
rect,
rounding: rounding.into(),
fill: fill_color.into(),
stroke: stroke.into(),
});
self.add(RectShape::new(rect, rounding, fill_color, stroke));
}
pub fn rect_filled(
@ -325,12 +320,7 @@ impl Painter {
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
) {
self.add(RectShape {
rect,
rounding: rounding.into(),
fill: fill_color.into(),
stroke: Default::default(),
});
self.add(RectShape::filled(rect, rounding, fill_color));
}
pub fn rect_stroke(
@ -339,12 +329,7 @@ impl Painter {
rounding: impl Into<Rounding>,
stroke: impl Into<Stroke>,
) {
self.add(RectShape {
rect,
rounding: rounding.into(),
fill: Default::default(),
stroke: stroke.into(),
});
self.add(RectShape::stroke(rect, rounding, stroke));
}
/// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`.

View File

@ -318,12 +318,12 @@ impl<'a> Widget for Checkbox<'a> {
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
let visuals = ui.style().interact(&response);
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
ui.painter().add(epaint::RectShape {
rect: big_icon_rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
});
ui.painter().add(epaint::RectShape::new(
big_icon_rect.expand(visuals.expansion),
visuals.rounding,
visuals.bg_fill,
visuals.bg_stroke,
));
if *checked {
// Check mark:
@ -535,7 +535,7 @@ impl Widget for ImageButton {
let selection = ui.visuals().selection;
(
Vec2::ZERO,
Rounding::none(),
Rounding::ZERO,
selection.bg_fill,
selection.stroke,
)
@ -552,6 +552,8 @@ impl Widget for ImageButton {
Default::default()
};
let image = image.rounding(rounding); // apply rounding to the image
// Draw frame background (for transparent images):
ui.painter()
.rect_filled(rect.expand2(expansion), rounding, fill);

View File

@ -42,6 +42,7 @@ pub struct Image {
tint: Color32,
sense: Sense,
rotation: Option<(Rot2, Vec2)>,
rounding: Rounding,
}
impl Image {
@ -54,6 +55,7 @@ impl Image {
tint: Color32::WHITE,
sense: Sense::hover(),
rotation: None,
rounding: Rounding::ZERO,
}
}
@ -89,8 +91,26 @@ impl Image {
/// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right).
///
/// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin.
///
/// Due to limitations in the current implementation,
/// this will turn off rounding of the image.
pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
self.rotation = Some((Rot2::from_angle(angle), origin));
self.rounding = Rounding::ZERO; // incompatible with rotation
self
}
/// Round the corners of the image.
///
/// The default is no rounding ([`Rounding::ZERO`]).
///
/// Due to limitations in the current implementation,
/// this will turn off any rotation of the image.
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.rounding = rounding.into();
if self.rounding != Rounding::ZERO {
self.rotation = None; // incompatible with rounding
}
self
}
}
@ -111,6 +131,7 @@ impl Image {
tint,
sense: _,
rotation,
rounding,
} = self;
if *bg_fill != Default::default() {
@ -119,14 +140,27 @@ impl Image {
ui.painter().add(Shape::mesh(mesh));
}
{
// TODO(emilk): builder pattern for Mesh
if let Some((rot, origin)) = rotation {
// TODO(emilk): implement this using `PathShape` (add texture support to it).
// This will also give us anti-aliasing of rotated images.
egui_assert!(
*rounding == Rounding::ZERO,
"Image had both rounding and rotation. Please pick only one"
);
let mut mesh = Mesh::with_texture(*texture_id);
mesh.add_rect_with_uv(rect, *uv, *tint);
if let Some((rot, origin)) = rotation {
mesh.rotate(*rot, rect.min + *origin * *size);
}
mesh.rotate(*rot, rect.min + *origin * *size);
ui.painter().add(Shape::mesh(mesh));
} else {
ui.painter().add(RectShape {
rect,
rounding: *rounding,
fill: *tint,
stroke: Stroke::NONE,
fill_texture_id: *texture_id,
uv: *uv,
});
}
}
}

View File

@ -127,12 +127,7 @@ impl Bar {
};
let rect = transform.rect_from_values(&self.bounds_min(), &self.bounds_max());
let rect = Shape::Rect(RectShape {
rect,
rounding: Rounding::none(),
fill,
stroke,
});
let rect = Shape::Rect(RectShape::new(rect, Rounding::ZERO, fill, stroke));
shapes.push(rect);
}

View File

@ -150,12 +150,7 @@ impl BoxElem {
&self.point_at(self.argument - self.box_width / 2.0, self.spread.quartile1),
&self.point_at(self.argument + self.box_width / 2.0, self.spread.quartile3),
);
let rect = Shape::Rect(RectShape {
rect,
rounding: Rounding::none(),
fill,
stroke,
});
let rect = Shape::Rect(RectShape::new(rect, Rounding::ZERO, fill, stroke));
shapes.push(rect);
let line_between = |v1, v2| {

View File

@ -864,12 +864,14 @@ impl Plot {
// Background
if show_background {
ui.painter().with_clip_rect(rect).add(epaint::RectShape {
rect,
rounding: Rounding::same(2.0),
fill: ui.visuals().extreme_bg_color,
stroke: ui.visuals().widgets.noninteractive.bg_stroke,
});
ui.painter()
.with_clip_rect(rect)
.add(epaint::RectShape::new(
rect,
Rounding::same(2.0),
ui.visuals().extreme_bg_color,
ui.visuals().widgets.noninteractive.bg_stroke,
));
}
// --- Legend ---

View File

@ -368,31 +368,27 @@ impl<'t> TextEdit<'t> {
let frame_rect = frame_rect.expand(visuals.expansion);
let shape = if is_mutable {
if output.response.has_focus() {
epaint::RectShape {
rect: frame_rect,
rounding: visuals.rounding,
// fill: ui.visuals().selection.bg_fill,
fill: ui.visuals().extreme_bg_color,
stroke: ui.visuals().selection.stroke,
}
epaint::RectShape::new(
frame_rect,
visuals.rounding,
ui.visuals().extreme_bg_color,
ui.visuals().selection.stroke,
)
} else {
epaint::RectShape {
rect: frame_rect,
rounding: visuals.rounding,
fill: ui.visuals().extreme_bg_color,
stroke: visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
}
epaint::RectShape::new(
frame_rect,
visuals.rounding,
ui.visuals().extreme_bg_color,
visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
)
}
} else {
let visuals = &ui.style().visuals.widgets.inactive;
epaint::RectShape {
rect: frame_rect,
rounding: visuals.rounding,
// fill: ui.visuals().extreme_bg_color,
// fill: visuals.bg_fill,
fill: Color32::TRANSPARENT,
stroke: visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
}
epaint::RectShape::stroke(
frame_rect,
visuals.rounding,
visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
)
};
ui.painter().set(where_to_put_background, shape);

View File

@ -70,12 +70,12 @@ impl FrameHistory {
let to_screen = emath::RectTransform::from_to(graph_rect, rect);
let mut shapes = Vec::with_capacity(3 + 2 * history.len());
shapes.push(Shape::Rect(epaint::RectShape {
shapes.push(Shape::Rect(epaint::RectShape::new(
rect,
rounding: style.rounding,
fill: ui.visuals().extreme_bg_color,
stroke: ui.style().noninteractive().bg_stroke,
}));
style.rounding,
ui.visuals().extreme_bg_color,
ui.style().noninteractive().bg_stroke,
)));
let rect = rect.shrink(4.0);
let color = ui.visuals().text_color();

View File

@ -64,12 +64,7 @@ pub fn drop_target<R>(
ui.painter().set(
where_to_put_background,
epaint::RectShape {
rounding: style.rounding,
fill,
stroke,
rect,
},
epaint::RectShape::new(rect, style.rounding, fill, stroke),
);
InnerResponse::new(ret, response)

View File

@ -8,7 +8,7 @@ fn main() {
let mut egui_glium = egui_glium::EguiGlium::new(&display, &event_loop);
let png_data = include_bytes!("../../../examples/retained_image/src/rust-logo-256x256.png");
let png_data = include_bytes!("../../../examples/retained_image/src/crab.png");
let image = load_glium_image(png_data);
let image_size = egui::vec2(image.width as f32, image.height as f32);
// Load to gpu memory

View File

@ -58,6 +58,12 @@ impl Rect {
max: pos2(f32::NAN, f32::NAN),
};
/// A [`Rect`] filled with zeroes.
pub const ZERO: Self = Self {
min: Pos2::ZERO,
max: Pos2::ZERO,
};
#[inline(always)]
pub const fn from_min_max(min: Pos2, max: Pos2) -> Self {
Rect { min, max }

View File

@ -120,13 +120,13 @@ impl Mesh {
pub fn append_ref(&mut self, other: &Mesh) {
crate::epaint_assert!(other.is_valid());
if !self.is_empty() {
if self.is_empty() {
self.texture_id = other.texture_id;
} else {
assert_eq!(
self.texture_id, other.texture_id,
"Can't merge Mesh using different textures"
);
} else {
self.texture_id = other.texture_id;
}
let index_offset = self.vertices.len() as u32;

View File

@ -282,6 +282,8 @@ impl Shape {
pub fn texture_id(&self) -> super::TextureId {
if let Shape::Mesh(mesh) = self {
mesh.texture_id
} else if let Shape::Rect(rect_shape) = self {
rect_shape.fill_texture_id
} else {
super::TextureId::default()
}
@ -406,6 +408,8 @@ pub struct PathShape {
/// Color and thickness of the line.
pub stroke: Stroke,
// TODO(emilk): Add texture support either by supplying uv for each point,
// or by some transform from points to uv (e.g. a callback or a linear transform matrix).
}
impl PathShape {
@ -476,7 +480,7 @@ impl From<PathShape> for Shape {
pub struct RectShape {
pub rect: Rect,
/// How rounded the corners are. Use `Rounding::none()` for no rounding.
/// How rounded the corners are. Use `Rounding::ZERO` for no rounding.
pub rounding: Rounding,
/// How to fill the rectangle.
@ -484,9 +488,37 @@ pub struct RectShape {
/// The thickness and color of the outline.
pub stroke: Stroke,
/// If the rect should be filled with a texture, which one?
///
/// The texture is multiplied with [`Self::fill`].
pub fill_texture_id: TextureId,
/// What UV coordinates to use for the texture?
///
/// To display a texture, set [`Self::fill_texture_id`],
/// and set this to `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`.
pub uv: Rect,
}
impl RectShape {
#[inline]
pub fn new(
rect: Rect,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
stroke: impl Into<Stroke>,
) -> Self {
Self {
rect,
rounding: rounding.into(),
fill: fill_color.into(),
stroke: stroke.into(),
fill_texture_id: Default::default(),
uv: Rect::ZERO,
}
}
#[inline]
pub fn filled(
rect: Rect,
@ -498,6 +530,8 @@ impl RectShape {
rounding: rounding.into(),
fill: fill_color.into(),
stroke: Default::default(),
fill_texture_id: Default::default(),
uv: Rect::ZERO,
}
}
@ -508,6 +542,8 @@ impl RectShape {
rounding: rounding.into(),
fill: Default::default(),
stroke: stroke.into(),
fill_texture_id: Default::default(),
uv: Rect::ZERO,
}
}
@ -549,7 +585,7 @@ pub struct Rounding {
impl Default for Rounding {
#[inline]
fn default() -> Self {
Self::none()
Self::ZERO
}
}
@ -566,6 +602,14 @@ impl From<f32> for Rounding {
}
impl Rounding {
/// No rounding on any corner.
pub const ZERO: Self = Self {
nw: 0.0,
ne: 0.0,
sw: 0.0,
se: 0.0,
};
#[inline]
pub fn same(radius: f32) -> Self {
Self {
@ -577,6 +621,7 @@ impl Rounding {
}
#[inline]
#[deprecated = "Use Rounding::ZERO"]
pub fn none() -> Self {
Self {
nw: 0.0,

View File

@ -492,6 +492,20 @@ impl Path {
pub fn fill(&mut self, feathering: f32, color: Color32, out: &mut Mesh) {
fill_closed_path(feathering, &mut self.0, color, out);
}
/// Like [`Self::fill`] but with texturing.
///
/// The `uv_from_pos` is called for each vertex position.
pub fn fill_with_uv(
&mut self,
feathering: f32,
color: Color32,
texture_id: TextureId,
uv_from_pos: impl Fn(Pos2) -> Pos2,
out: &mut Mesh,
) {
fill_closed_path_with_uv(feathering, &mut self.0, color, texture_id, uv_from_pos, out);
}
}
pub mod path {
@ -508,7 +522,7 @@ pub mod path {
let r = clamp_radius(rounding, rect);
if r == Rounding::none() {
if r == Rounding::ZERO {
let min = rect.min;
let max = rect.max;
path.reserve(4);
@ -728,6 +742,89 @@ fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out
}
}
/// Like [`fill_closed_path`] but with texturing.
///
/// The `uv_from_pos` is called for each vertex position.
fn fill_closed_path_with_uv(
feathering: f32,
path: &mut [PathPoint],
color: Color32,
texture_id: TextureId,
uv_from_pos: impl Fn(Pos2) -> Pos2,
out: &mut Mesh,
) {
if color == Color32::TRANSPARENT {
return;
}
if out.is_empty() {
out.texture_id = texture_id;
} else {
assert_eq!(
out.texture_id, texture_id,
"Mixing different `texture_id` in the same "
);
}
let n = path.len() as u32;
if feathering > 0.0 {
if cw_signed_area(path) < 0.0 {
// Wrong winding order - fix:
path.reverse();
for point in path.iter_mut() {
point.normal = -point.normal;
}
}
out.reserve_triangles(3 * n as usize);
out.reserve_vertices(2 * n as usize);
let color_outer = Color32::TRANSPARENT;
let idx_inner = out.vertices.len() as u32;
let idx_outer = idx_inner + 1;
// The fill:
for i in 2..n {
out.add_triangle(idx_inner + 2 * (i - 1), idx_inner, idx_inner + 2 * i);
}
// The feathering:
let mut i0 = n - 1;
for i1 in 0..n {
let p1 = &path[i1 as usize];
let dm = 0.5 * feathering * p1.normal;
let pos = p1.pos - dm;
out.vertices.push(Vertex {
pos,
uv: uv_from_pos(pos),
color,
});
let pos = p1.pos + dm;
out.vertices.push(Vertex {
pos,
uv: uv_from_pos(pos),
color: color_outer,
});
out.add_triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0);
out.add_triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1);
i0 = i1;
}
} else {
out.reserve_triangles(n as usize);
let idx = out.vertices.len() as u32;
out.vertices.extend(path.iter().map(|p| Vertex {
pos: p.pos,
uv: uv_from_pos(p.pos),
color,
}));
for i in 2..n {
out.add_triangle(idx, idx + i - 1, idx + i);
}
}
}
/// Tessellate the given path as a stroke with thickness.
fn stroke_path(
feathering: f32,
@ -1304,6 +1401,8 @@ impl Tessellator {
rounding,
fill,
stroke,
fill_texture_id,
uv,
} = *rect;
if self.options.coarse_tessellation_culling
@ -1345,7 +1444,21 @@ impl Tessellator {
path.clear();
path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding);
path.add_line_loop(&self.scratchpad_points);
path.fill(self.feathering, fill, out);
if uv.is_positive() {
// Textured
let uv_from_pos = |p: Pos2| {
pos2(
remap(p.x, rect.x_range(), uv.x_range()),
remap(p.y, rect.y_range(), uv.y_range()),
)
};
path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out);
} else {
// Untextured
path.fill(self.feathering, fill, out);
}
path.stroke_closed(self.feathering, stroke, out);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@ -6,7 +6,7 @@ use egui_extras::RetainedImage;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(300.0, 900.0)),
initial_window_size: Some(egui::vec2(400.0, 1000.0)),
..Default::default()
};
eframe::run_native(
@ -18,48 +18,66 @@ fn main() -> Result<(), eframe::Error> {
struct MyApp {
image: RetainedImage,
rounding: f32,
tint: egui::Color32,
}
impl Default for MyApp {
fn default() -> Self {
Self {
image: RetainedImage::from_image_bytes(
"rust-logo-256x256.png",
include_bytes!("rust-logo-256x256.png"),
)
.unwrap(),
tint: egui::Color32::from_rgb(255, 0, 255),
// crab image is CC0, found on https://stocksnap.io/search/crab
image: RetainedImage::from_image_bytes("crab.png", include_bytes!("crab.png")).unwrap(),
rounding: 32.0,
tint: egui::Color32::from_rgb(100, 200, 200),
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let Self {
image,
rounding,
tint,
} = self;
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("This is an image:");
self.image.show(ui);
image.show(ui);
ui.heading("This is a rotated image with a tint:");
ui.add_space(32.0);
ui.heading("This is a tinted image with rounded corners:");
ui.add(
egui::Image::new(self.image.texture_id(ctx), self.image.size_vec2())
.rotate(45.0_f32.to_radians(), egui::Vec2::splat(0.5))
.tint(self.tint),
egui::Image::new(image.texture_id(ctx), image.size_vec2())
.tint(*tint)
.rounding(*rounding),
);
ui.horizontal(|ui| {
ui.label("Tint:");
egui::color_picker::color_edit_button_srgba(
ui,
&mut self.tint,
tint,
egui::color_picker::Alpha::BlendOrAdditive,
);
ui.add_space(16.0);
ui.label("Rounding:");
ui.add(
egui::DragValue::new(rounding)
.speed(1.0)
.clamp_range(0.0..=0.5 * image.size_vec2().min_elem()),
);
});
ui.add_space(32.0);
ui.heading("This is an image you can click:");
ui.add(egui::ImageButton::new(
self.image.texture_id(ctx),
self.image.size_vec2(),
image.texture_id(ctx),
image.size_vec2(),
));
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@ -1 +0,0 @@
Rust logo by Mozilla, from https://github.com/rust-lang/rust-artwork