1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 13:50:04 -04:00

Merge branch 'main' into lucas/text-edit-min-size

# Conflicts:
#	tests/egui_tests/tests/snapshots/visuals/drag_value.png
This commit is contained in:
Lucas Meurer
2026-08-31 15:01:57 +02:00
128 changed files with 1800 additions and 699 deletions

View File

@@ -1,4 +1,4 @@
use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
use crate::{Rgba, fast_round, mul_frac_round};
/// This format is used for space-efficient color representation (32 bits).
///
@@ -131,35 +131,10 @@ impl Color32 {
/// but for transparent colors what you get back might be slightly different (rounding errors).
#[inline]
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
use std::sync::OnceLock;
match a {
// common-case optimization:
0 => Self::TRANSPARENT,
// common-case optimization:
255 => Self::from_rgb(r, g, b),
a => {
static LOOKUP_TABLE: OnceLock<Box<[u8]>> = OnceLock::new();
let lut = LOOKUP_TABLE.get_or_init(|| {
(0..=u16::MAX)
.map(|i| {
let [value, alpha] = i.to_ne_bytes();
fast_round(value as f32 * linear_f32_from_linear_u8(alpha))
})
.collect()
});
let [r, g, b] =
[r, g, b].map(|value| lut[usize::from(u16::from_ne_bytes([value, a]))]);
Self::from_rgba_premultiplied(r, g, b, a)
}
}
Self::from_rgba_unmultiplied_const(r, g, b, a)
}
/// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context.
///
/// It is slightly slower when operating on non-const data.
/// This is the same as [`Self::from_rgba_unmultiplied`], but for const contexts.
#[inline]
pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self {
match a {
@@ -170,9 +145,9 @@ impl Color32 {
255 => Self::from_rgb(r, g, b),
a => {
let r = fast_round(r as f32 * linear_f32_from_linear_u8(a));
let g = fast_round(g as f32 * linear_f32_from_linear_u8(a));
let b = fast_round(b as f32 * linear_f32_from_linear_u8(a));
let r = mul_frac_round(r, a);
let g = mul_frac_round(g, a);
let b = mul_frac_round(b, a);
Self::from_rgba_premultiplied(r, g, b, a)
}
}
@@ -535,4 +510,14 @@ mod test {
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
);
}
#[test]
fn mul_frac_round_vs_old() {
for x in (0..=255u8).step_by(4) {
for a in (1..=255u8).step_by(4) {
let old = fast_round(x as f32 * crate::linear_f32_from_linear_u8(a));
assert_eq!(old, mul_frac_round(x, a));
}
}
}
}

View File

@@ -134,6 +134,17 @@ const fn fast_round(r: f32) -> u8 {
(r + 0.5) as _ // rust does a saturating cast since 1.45
}
/// Compute val * (frac/255) with no floating point or divisions.
#[inline]
const fn mul_frac_round(val: u8, frac: u8) -> u8 {
// Treat this as a simple fixed point calculation
let p = (val as u16) * (frac as u16) + 128;
((p + (p >> 8)) >> 8) as u8
// Logic split out a bit more.
//let p = (val as u16) * (frac as u16) + 127; // + 127 to round or remove to truncate.
//return ((p + 1 + (p >> 8)) >> 8) as u8;
}
#[test]
pub fn test_srgba_conversion() {
for b in 0..=255 {

View File

@@ -53,6 +53,12 @@ android-native-activity = ["egui-winit/android-native-activity"]
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
## Enable experimental egui features that might have massive breaking changes or be removed entirely in future updates.
## Enabling this won't break semver, it's just future compatibility risk.
##
## Currently, this enables the theme plugin.
experimental = ["egui/experimental"]
## Enable [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
##
## There is generally no need to enable both the `wgpu` and `glow` features,

View File

@@ -1094,9 +1094,10 @@ impl GlutinWindowContext {
//
// The justification for FallbackEgl over PreferEgl is at https://github.com/emilk/egui/pull/2526#issuecomment-1400229576 .
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
egui_ctx,
viewport_builder.clone(),
.with_window_attributes(Some(egui_winit::apply_monitor_to_window_attributes(
egui_winit::create_winit_window_attributes(egui_ctx, viewport_builder.clone()),
&viewport_builder,
event_loop,
)));
let (window, gl_config) = {
@@ -1262,17 +1263,40 @@ impl GlutinWindowContext {
window
} else {
log::debug!("Creating a window for viewport {viewport_id:?}");
let window_attributes = egui_winit::create_winit_window_attributes(
&self.egui_ctx,
viewport.builder.clone(),
let window_attributes = egui_winit::apply_monitor_to_window_attributes(
egui_winit::create_winit_window_attributes(
&self.egui_ctx,
viewport.builder.clone(),
),
&viewport.builder,
event_loop,
);
if window_attributes.transparent()
&& self.gl_config.supports_transparency() == Some(false)
&& !cfg!(target_os = "windows")
{
log::error!("Cannot create transparent window: the GL config does not support it");
}
let window =
glutin_winit::finalize_window(event_loop, window_attributes, &self.gl_config)?;
let window = cfg_select! {
target_os = "windows" => {
if viewport_id != ViewportId::ROOT && window_attributes.transparent() {
// Preserve explicitly requested transparent child viewports on Windows.
// Some GL paths report no transparency support although composition works.
event_loop.create_window(window_attributes)?
} else {
glutin_winit::finalize_window(
event_loop,
window_attributes,
&self.gl_config,
)?
}
}
_ => {
// Keep the normal platform-specific finalization path elsewhere.
glutin_winit::finalize_window(event_loop, window_attributes, &self.gl_config)?
}
};
egui_winit::apply_viewport_builder_to_window(
&self.egui_ctx,
&window,
@@ -1497,9 +1521,17 @@ fn initialize_or_update_viewport(
.and_then(|vp| vp.builder.icon.clone());
}
let root_transparent = viewports
.get(&ViewportId::ROOT)
.and_then(|viewport| viewport.builder.transparent);
match viewports.entry(ids.this) {
Entry::Vacant(entry) => {
// New viewport:
if ids.this != ViewportId::ROOT && builder.transparent.is_none() {
// Child viewports inherit the root setting unless they explicitly override it.
builder.transparent = root_transparent;
}
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
entry.insert(Viewport {
ids,

View File

@@ -98,24 +98,64 @@ fn vs_main(
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
@group(1) @binding(1) var r_tex_sampler: sampler;
/// Set in bit 0 of `r_tex_flags` if the sampler uses nearest filtering.
///
/// Must match `TEX_FLAG_NEAREST` in `renderer.rs`.
const TEX_FLAG_NEAREST: u32 = 1u;
/// Wrap modes, stored in bits 1+ of `r_tex_flags`.
///
/// Must match the `WRAP_MODE_*` constants in `renderer.rs`.
const WRAP_MODE_CLAMP_TO_EDGE: u32 = 0u;
const WRAP_MODE_REPEAT: u32 = 1u;
const WRAP_MODE_MIRRORED_REPEAT: u32 = 2u;
/// Texture flags, only read when `predictable_texture_filtering` is on.
///
/// Bit 0: `TEX_FLAG_NEAREST`.
/// Bits 1+: one of the `WRAP_MODE_*` constants.
@group(1) @binding(2) var<uniform> r_tex_flags: vec4<u32>;
/// Map a texel coordinate to a valid texel according to the texture's wrap mode.
fn wrap_texel_coord(coord: vec2<i32>, texture_size: vec2<i32>) -> vec2<i32> {
let wrap_mode = r_tex_flags[0] >> 1u;
if wrap_mode == WRAP_MODE_REPEAT {
return ((coord % texture_size) + texture_size) % texture_size;
} else if wrap_mode == WRAP_MODE_MIRRORED_REPEAT {
let period = 2 * texture_size;
let phase = ((coord % period) + period) % period;
return min(phase, period - vec2<i32>(1, 1) - phase);
} else {
// WRAP_MODE_CLAMP_TO_EDGE
return clamp(coord, vec2<i32>(0, 0), texture_size - vec2<i32>(1, 1));
}
}
fn sample_texture(in: VertexOutput) -> vec4<f32> {
if r_locals.predictable_texture_filtering == 0 {
// Hardware filtering: fast, but varies across GPUs and drivers.
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
} else {
// Manual bilinear filtering with four taps at pixel centers using textureLoad
let texture_size = vec2<i32>(textureDimensions(r_tex_color, 0));
let texture_size_f = vec2<f32>(texture_size);
if (r_tex_flags[0] & TEX_FLAG_NEAREST) != 0u {
// Nearest filtering: load the texel under the sample position.
let texel = wrap_texel_coord(vec2<i32>(floor(in.tex_coord * texture_size_f)), texture_size);
return textureLoad(r_tex_color, texel, 0);
}
// Manual bilinear filtering with four taps at pixel centers using textureLoad
let pixel_coord = in.tex_coord * texture_size_f - 0.5;
let pixel_fract = fract(pixel_coord);
let pixel_floor = vec2<i32>(floor(pixel_coord));
// Manual texture clamping
let max_coord = texture_size - vec2<i32>(1, 1);
let p00 = clamp(pixel_floor + vec2<i32>(0, 0), vec2<i32>(0, 0), max_coord);
let p10 = clamp(pixel_floor + vec2<i32>(1, 0), vec2<i32>(0, 0), max_coord);
let p01 = clamp(pixel_floor + vec2<i32>(0, 1), vec2<i32>(0, 0), max_coord);
let p11 = clamp(pixel_floor + vec2<i32>(1, 1), vec2<i32>(0, 0), max_coord);
let p00 = wrap_texel_coord(pixel_floor + vec2<i32>(0, 0), texture_size);
let p10 = wrap_texel_coord(pixel_floor + vec2<i32>(1, 0), texture_size);
let p01 = wrap_texel_coord(pixel_floor + vec2<i32>(0, 1), texture_size);
let p11 = wrap_texel_coord(pixel_floor + vec2<i32>(1, 1), texture_size);
// Load at pixel centers
let tl = textureLoad(r_tex_color, p00, 0);

View File

@@ -245,6 +245,11 @@ pub struct Renderer {
uniform_bind_group: wgpu::BindGroup,
texture_bind_group_layout: wgpu::BindGroupLayout,
/// Uniform buffers each holding a single `u32` of texture flags
/// (see [`texture_flags`]), indexed by that flag value.
/// Read by the shader when `predictable_texture_filtering` is on.
texture_flag_buffers: [wgpu::Buffer; NUM_TEXTURE_FLAGS],
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
/// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided
/// sampler.
@@ -347,10 +352,31 @@ impl Renderer {
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
(core::mem::size_of::<u32>() * 4) as _,
),
ty: wgpu::BufferBindingType::Uniform,
},
count: None,
},
],
})
};
let texture_flag_buffers = core::array::from_fn::<_, NUM_TEXTURE_FLAGS, _>(|flag| {
let flag = flag as u32;
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("egui_texture_flags_{flag}")),
contents: bytemuck::bytes_of(&[flag, 0, 0, 0]),
usage: wgpu::BufferUsages::UNIFORM,
})
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("egui_pipeline_layout"),
bind_group_layouts: &[
@@ -458,6 +484,7 @@ impl Renderer {
previous_uniform_buffer_content: UniformBuffer::zeroed(),
uniform_bind_group,
texture_bind_group_layout,
texture_flag_buffers,
textures: HashMap::default(),
next_user_texture_id: 0,
samplers: HashMap::default(),
@@ -709,6 +736,9 @@ impl Renderer {
};
let bind_group = bind_group.unwrap_or_else(|| {
let nearest =
image_delta.options.magnification == epaint::textures::TextureFilter::Nearest;
let wrap_mode = wrap_mode_flag(image_delta.options.wrap_mode);
let sampler = self
.samplers
.entry(image_delta.options)
@@ -727,6 +757,11 @@ impl Renderer {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
.as_entire_binding(),
},
],
})
});
@@ -829,6 +864,8 @@ impl Renderer {
) -> epaint::TextureId {
profiling::function_scope!();
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
let wrap_mode = address_mode_wrap_flag(sampler_descriptor.address_mode_u);
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
@@ -846,6 +883,11 @@ impl Renderer {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
.as_entire_binding(),
},
],
});
@@ -885,6 +927,8 @@ impl Renderer {
.get_mut(&id)
.expect("Tried to update a texture that has not been allocated yet.");
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
let wrap_mode = address_mode_wrap_flag(sampler_descriptor.address_mode_u);
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
@@ -902,6 +946,11 @@ impl Renderer {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
.as_entire_binding(),
},
],
});
@@ -1080,6 +1129,49 @@ impl Renderer {
}
}
/// Set in bit 0 of the texture flags if the sampler uses nearest filtering.
///
/// Must match `TEX_FLAG_NEAREST` in `egui.wgsl`.
const TEX_FLAG_NEAREST: u32 = 1;
/// Wrap modes, stored in bits 1+ of the texture flags.
///
/// Must match the `WRAP_MODE_*` constants in `egui.wgsl`.
const WRAP_MODE_CLAMP_TO_EDGE: u32 = 0;
const WRAP_MODE_REPEAT: u32 = 1;
const WRAP_MODE_MIRRORED_REPEAT: u32 = 2;
/// Number of distinct values [`texture_flags`] can return.
const NUM_TEXTURE_FLAGS: usize = 6;
fn wrap_mode_flag(wrap_mode: epaint::textures::TextureWrapMode) -> u32 {
match wrap_mode {
epaint::textures::TextureWrapMode::ClampToEdge => WRAP_MODE_CLAMP_TO_EDGE,
epaint::textures::TextureWrapMode::Repeat => WRAP_MODE_REPEAT,
epaint::textures::TextureWrapMode::MirroredRepeat => WRAP_MODE_MIRRORED_REPEAT,
}
}
fn address_mode_wrap_flag(address_mode: wgpu::AddressMode) -> u32 {
match address_mode {
wgpu::AddressMode::Repeat => WRAP_MODE_REPEAT,
wgpu::AddressMode::MirrorRepeat => WRAP_MODE_MIRRORED_REPEAT,
wgpu::AddressMode::ClampToEdge | wgpu::AddressMode::ClampToBorder => {
WRAP_MODE_CLAMP_TO_EDGE
}
}
}
/// Index into [`Renderer::texture_flag_buffers`]: the texture flags
/// read by the shader when `predictable_texture_filtering` is on.
///
/// Bit 0: [`TEX_FLAG_NEAREST`].
/// Bits 1+: one of the `WRAP_MODE_*` constants.
fn texture_flags(nearest: bool, wrap_mode: u32) -> usize {
let nearest = if nearest { TEX_FLAG_NEAREST } else { 0 };
(nearest | (wrap_mode << 1)) as usize
}
fn create_sampler(
options: epaint::textures::TextureOptions,
device: &wgpu::Device,

View File

@@ -101,7 +101,10 @@ impl WgpuSetup {
let mut backends = create_new.instance_descriptor.backends;
// Don't try WebGPU if we're not in a secure context.
#[cfg(target_arch = "wasm32")]
// Emscripten is excluded: wgpu gates both its `webgpu` backend and
// its `web_sys` re-export on `not(Emscripten)` (see the cfg aliases
// in `wgpu/build.rs`).
#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
if backends.contains(wgpu::Backends::BROWSER_WEBGPU) {
let is_secure_context =
wgpu::web_sys::window().is_some_and(|w| w.is_secure_context());

View File

@@ -1867,7 +1867,9 @@ fn process_viewport_command(
#[cfg(target_os = "windows")]
{
use winit::platform::windows::WindowExtWindows as _;
window.set_undecorated_shadow(!v);
// don't request the undecorated-window drop shadow in fullscreen (#8399)
window.set_undecorated_shadow(!v && window.fullscreen().is_none());
}
}
ViewportCommand::WindowLevel(l) => window.set_window_level(match l {
@@ -1971,12 +1973,33 @@ pub fn create_window(
) -> Result<Window, winit::error::OsError> {
profiling::function_scope!();
let mut window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
let window_attributes = apply_monitor_to_window_attributes(
create_winit_window_attributes(egui_ctx, viewport_builder.clone()),
viewport_builder,
event_loop,
);
// Resolve target monitor index → MonitorHandle, so the window is created
// directly in borderless fullscreen on the requested output. This is the
// only reliable way to target a specific monitor under Wayland, and also
// avoids the Mutter race where OuterPosition is ignored pre-mapping.
let window = event_loop.create_window(window_attributes)?;
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
Ok(window)
}
/// Apply [`ViewportBuilder::with_monitor`] to freshly-built [`winit::window::WindowAttributes`].
///
/// Resolve the target monitor index → `MonitorHandle` and request borderless
/// fullscreen on that output, so the window is created directly on the right
/// monitor. This is the only reliable way to target a specific monitor under
/// Wayland, and also avoids the Mutter race where `OuterPosition` is ignored
/// pre-mapping.
///
/// Must be called by every backend that builds its own window from
/// [`create_winit_window_attributes`] (the glow backend and per-viewport window
/// creation do this) — otherwise `with_monitor` silently does nothing there.
pub fn apply_monitor_to_window_attributes(
mut window_attributes: winit::window::WindowAttributes,
viewport_builder: &ViewportBuilder,
event_loop: &ActiveEventLoop,
) -> winit::window::WindowAttributes {
if let Some(idx) = viewport_builder.monitor {
if let Some(monitor) = event_loop.available_monitors().nth(idx) {
window_attributes = window_attributes
@@ -1988,10 +2011,7 @@ pub fn create_window(
);
}
}
let window = event_loop.create_window(window_attributes)?;
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
Ok(window)
window_attributes
}
pub fn create_winit_window_attributes(
@@ -2171,7 +2191,10 @@ pub fn create_winit_window_attributes(
if let Some(show) = _taskbar {
window_attributes = window_attributes.with_skip_taskbar(!show);
}
window_attributes = window_attributes.with_undecorated_shadow(!decorations.unwrap_or(true));
// don't request the undecorated-window drop shadow in fullscreen (#8399)
let want_undecorated_shadow = !decorations.unwrap_or(true) && !fullscreen.unwrap_or(false);
window_attributes = window_attributes.with_undecorated_shadow(want_undecorated_shadow);
}
#[cfg(target_os = "macos")]

View File

@@ -45,6 +45,12 @@ color-hex = ["epaint/color-hex"]
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["epaint/default_fonts"]
## Enable experimental egui features that might have massive breaking changes or be removed entirely in future updates.
## Enabling this won't break semver, it's just future compatibility risk.
##
## Currently, this enables the theme plugin.
experimental = []
## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
mint = ["epaint/mint"]

View File

@@ -36,9 +36,10 @@ use crate::{
output::{FullOutput, LogicOutput},
pass_state::PassState,
plugin::{self, TypedPluginHandle},
resize, response, scroll_area,
resize, response, scroll_area, theme,
util::IdTypeMap,
viewport::ViewportClass,
widget_style::{StyleArgs, WidgetStyle},
};
use crate::IdMap;
@@ -412,6 +413,8 @@ struct ContextImpl {
is_accesskit_enabled: bool,
loaders: Arc<Loaders>,
themes: theme::Themes,
}
impl ContextImpl {
@@ -2093,6 +2096,48 @@ impl Context {
}
}
/// Experimental theming, gated behind the `experimental_theme` feature.
impl Context {
/// Register a [`StyleProvider`](crate::theme::StyleProvider) for the specified widget type.
///
/// A theme can only be added once for a specified widget.
/// If a theme is already registered for this widget, this is a no-op (useful for `eframe::run_simple_native`).
///
/// If you want to add the theme anyway, use [`Self::replace_widget_theme`] instead.
#[cfg(feature = "experimental")]
pub fn add_widget_theme<S: WidgetStyle + 'static>(
&self,
theme: impl theme::StyleProvider<S> + Send + Sync + 'static,
) {
self.write(|ctx| ctx.themes.register::<S>(theme, false));
}
/// Register a [`StyleProvider`](crate::theme::StyleProvider) for the specified widget.
///
/// Overwrite any theme already registered for the specified widget [`WidgetStyle`].
/// This allow to live edit a theme.
#[cfg(feature = "experimental")]
pub fn replace_widget_theme<S: WidgetStyle + 'static>(
&self,
theme: impl theme::StyleProvider<S> + Send + Sync + 'static,
) {
self.write(|ctx| ctx.themes.register::<S>(theme, true));
}
/// Compute the `WidgetStyle` using the registered theme.
///
/// The types you need to call this (e.g. `StyleArgs`) are only public
/// with the `experimental_theme` feature.
#[cfg_attr(not(feature = "experimental"), doc(hidden))]
pub fn get_widget_style<S: WidgetStyle + Clone + 'static>(
&self,
modifiers: &StyleArgs<'_>,
) -> S {
let theme = self.read(move |ctx| ctx.themes.get::<S>());
theme.lock().style(modifiers)
}
}
impl Context {
/// Tell `egui` which fonts to use.
///
@@ -2407,6 +2452,65 @@ impl Context {
TextureHandle::new(tex_mngr, tex_id)
}
/// Load a texture, or update a previously cached one if the image changed.
///
/// This is like [`Self::load_texture`], but caches the texture by `id`,
/// and only re-uploads the image when it changes.
/// This makes it safe to call every frame,
/// which is convenient for small, procedurally generated images.
///
/// The `id` must be globally unique for each cached image
/// (e.g. derived from a widget [`Id`]),
/// or the callers will fight over the same texture, re-uploading it every frame.
///
/// If this is not called for a full frame, the cache entry is evicted,
/// dropping both the cached [`ImageData`] (the CPU-side pixels)
/// and the cached [`TextureHandle`].
/// Dropping the handle also frees the texture itself,
/// unless you keep a clone of the handle alive.
pub fn load_texture_cached(
&self,
name: impl Into<String>,
id: Id,
image: impl Into<ImageData>,
options: TextureOptions,
) -> TextureHandle {
profiling::function_scope!();
use crate::cache::FramePublisher;
type TextureCache = FramePublisher<Id, (ImageData, TextureOptions, TextureHandle)>;
let image = image.into();
let cached: Option<(ImageData, TextureOptions, TextureHandle)> =
self.memory_mut(|mem| mem.caches.cache::<TextureCache>().get(&id).cloned());
let (image, handle) = match cached {
Some((cached_image, cached_options, handle))
if cached_image == image && cached_options == options =>
{
(cached_image, handle)
}
Some((_, _, mut handle)) => {
handle.set(image.clone(), options);
(image, handle)
}
None => {
let handle = self.load_texture(name, image.clone(), options);
(image, handle)
}
};
// (Re-)publish to keep the entry from being evicted:
self.memory_mut(|mem| {
mem.caches
.cache::<TextureCache>()
.set(id, (image, options, handle.clone()));
});
handle
}
/// Low-level texture manager.
///
/// In general it is easier to use [`Self::load_texture`] and [`TextureHandle`].

View File

@@ -417,13 +417,20 @@ pub mod response;
mod sense;
pub mod style;
pub mod text_selection;
#[cfg(feature = "experimental")]
pub mod theme;
#[cfg(not(feature = "experimental"))]
mod theme;
mod ui;
mod ui_builder;
mod ui_stack;
pub mod util;
pub mod viewport;
mod widget_rect;
#[cfg(feature = "experimental")]
pub mod widget_style;
#[cfg(not(feature = "experimental"))]
mod widget_style;
pub mod widget_text;
pub mod widgets;

View File

@@ -1685,7 +1685,7 @@ impl Widgets {
bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
corner_radius: CornerRadius::same(2),
corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
inactive: WidgetVisuals {
@@ -1693,7 +1693,7 @@ impl Widgets {
bg_fill: Color32::from_gray(60), // checkbox background
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
corner_radius: CornerRadius::same(2),
corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
hovered: WidgetVisuals {
@@ -1701,7 +1701,7 @@ impl Widgets {
bg_fill: Color32::from_gray(70),
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
corner_radius: CornerRadius::same(3),
corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
active: WidgetVisuals {
@@ -1709,7 +1709,7 @@ impl Widgets {
bg_fill: Color32::from_gray(55),
bg_stroke: Stroke::new(1.0, Color32::WHITE),
fg_stroke: Stroke::new(2.0, Color32::WHITE),
corner_radius: CornerRadius::same(2),
corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
open: WidgetVisuals {
@@ -1717,7 +1717,7 @@ impl Widgets {
bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
corner_radius: CornerRadius::same(2),
corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
}
@@ -1730,7 +1730,7 @@ impl Widgets {
bg_fill: Color32::from_gray(248),
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
corner_radius: CornerRadius::same(2),
corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
inactive: WidgetVisuals {
@@ -1738,7 +1738,7 @@ impl Widgets {
bg_fill: Color32::from_gray(230), // checkbox background
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
corner_radius: CornerRadius::same(2),
corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
hovered: WidgetVisuals {
@@ -1746,7 +1746,7 @@ impl Widgets {
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
fg_stroke: Stroke::new(1.5, Color32::BLACK),
corner_radius: CornerRadius::same(3),
corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
active: WidgetVisuals {
@@ -1754,7 +1754,7 @@ impl Widgets {
bg_fill: Color32::from_gray(165),
bg_stroke: Stroke::new(1.0, Color32::BLACK),
fg_stroke: Stroke::new(2.0, Color32::BLACK),
corner_radius: CornerRadius::same(2),
corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
open: WidgetVisuals {
@@ -1762,7 +1762,7 @@ impl Widgets {
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
fg_stroke: Stroke::new(1.0, Color32::BLACK),
corner_radius: CornerRadius::same(2),
corner_radius: CornerRadius::same(4),
expansion: 0.0,
},
}

View File

@@ -0,0 +1,156 @@
use emath::Vec2;
use epaint::{Shadow, Stroke, text::TextWrapMode};
use crate::{
Frame, TextStyle,
theme::StyleProvider,
widget_style::{
BaseStyle, ButtonStyle, CheckboxStyle, HasClasses as _, LabelStyle, SELECTED_CLASS,
SeparatorStyle, StyleArgs, TextVisuals, WidgetState,
},
};
/// The default [`StyleProvider`], implementing the default egui look based on
/// [`crate::style::WidgetVisuals`].
#[derive(Debug, Clone)]
pub struct DefaultStyle;
impl StyleProvider<BaseStyle> for DefaultStyle {
fn style(&mut self, modifiers: &StyleArgs<'_>) -> BaseStyle {
let StyleArgs { style, state, .. } = modifiers;
let spacing = &style.spacing;
let widget_visuals = match state {
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
WidgetState::Inactive => style.visuals.widgets.inactive,
WidgetState::Hovered => style.visuals.widgets.hovered,
WidgetState::Active => style.visuals.widgets.active,
};
BaseStyle {
frame: Frame {
fill: widget_visuals.bg_fill,
stroke: widget_visuals.bg_stroke,
corner_radius: widget_visuals.corner_radius,
inner_margin: spacing.button_padding.into(),
..Default::default()
},
stroke: widget_visuals.fg_stroke,
text: TextVisuals {
color: widget_visuals.text_color(),
font_id: modifiers
.style
.override_font_id
.clone()
.unwrap_or_else(|| TextStyle::Body.resolve(style)),
strikethrough: Stroke::NONE,
underline: Stroke::NONE,
},
}
}
}
impl StyleProvider<ButtonStyle> for DefaultStyle {
fn style(&mut self, modifiers: &StyleArgs<'_>) -> ButtonStyle {
let StyleArgs {
ctx,
classes,
style,
state,
..
} = modifiers;
let spacing = &style.spacing;
let mut widget_visuals = match state {
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
WidgetState::Inactive => style.visuals.widgets.inactive,
WidgetState::Hovered => style.visuals.widgets.hovered,
WidgetState::Active => style.visuals.widgets.active,
};
let mut ws: BaseStyle = ctx.get_widget_style(modifiers);
if classes.has(SELECTED_CLASS) {
let visuals = &style.visuals;
widget_visuals.weak_bg_fill = visuals.selection.bg_fill;
widget_visuals.bg_fill = visuals.selection.bg_fill;
widget_visuals.fg_stroke = visuals.selection.stroke;
ws.text.color = visuals.selection.stroke.color;
}
ButtonStyle {
frame: Frame {
fill: widget_visuals.weak_bg_fill,
stroke: widget_visuals.bg_stroke,
corner_radius: widget_visuals.corner_radius,
outer_margin: (-Vec2::splat(widget_visuals.expansion)).into(),
inner_margin: (spacing.button_padding + Vec2::splat(widget_visuals.expansion)
- Vec2::splat(widget_visuals.bg_stroke.width))
.into(),
..Default::default()
},
text_style: ws.text,
}
}
}
impl StyleProvider<CheckboxStyle> for DefaultStyle {
fn style(&mut self, modifiers: &StyleArgs<'_>) -> CheckboxStyle {
let StyleArgs {
ctx, style, state, ..
} = modifiers;
let spacing = &style.spacing;
let widget_visuals = match state {
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
WidgetState::Inactive => style.visuals.widgets.inactive,
WidgetState::Hovered => style.visuals.widgets.hovered,
WidgetState::Active => style.visuals.widgets.active,
};
let ws: BaseStyle = ctx.get_widget_style(modifiers);
CheckboxStyle {
frame: Frame::new(),
checkbox_size: spacing.icon_width,
check_size: spacing.icon_width_inner,
checkbox_frame: Frame {
fill: widget_visuals.bg_fill,
corner_radius: widget_visuals.corner_radius,
stroke: widget_visuals.bg_stroke,
..Default::default()
},
text_style: ws.text,
check_stroke: ws.stroke,
}
}
}
impl StyleProvider<LabelStyle> for DefaultStyle {
fn style(&mut self, modifiers: &StyleArgs<'_>) -> LabelStyle {
let StyleArgs { ctx, .. } = modifiers;
let ws: BaseStyle = ctx.get_widget_style(modifiers);
LabelStyle {
frame: Frame {
fill: ws.frame.fill,
inner_margin: 0.0.into(),
outer_margin: 0.0.into(),
stroke: Stroke::NONE,
shadow: Shadow::NONE,
corner_radius: 0.into(),
},
text: ws.text,
wrap_mode: TextWrapMode::Wrap,
}
}
}
impl StyleProvider<SeparatorStyle> for DefaultStyle {
fn style(&mut self, modifiers: &StyleArgs<'_>) -> SeparatorStyle {
let StyleArgs { style, .. } = modifiers;
SeparatorStyle {
spacing: 6.0,
// A separator is never interactive, so its stroke doesn't depend on the widget state:
stroke: style.visuals.widgets.noninteractive.bg_stroke,
}
}
}

View File

@@ -0,0 +1,48 @@
//! Theming: pluggable [`StyleProvider`]s that compute the style of each widget.
// This module is only public with the `experimental_theme` feature,
// so without it a lot of it looks unused:
#![cfg_attr(not(feature = "experimental"), allow(dead_code, unused_imports))]
mod default_style;
mod style_provider;
mod themes;
pub use self::{default_style::DefaultStyle, style_provider::StyleProvider, themes::Themes};
use crate::{
Ui,
widget_style::{Classes, StyleArgs, WidgetState, WidgetStyle},
};
impl Ui {
/// The style of the widget with the given [`crate::Id`] and `Classes`,
/// as computed by the registered theme.
///
/// The types you need to call this are only public with the
/// `experimental_theme` feature.
#[cfg_attr(not(feature = "experimental"), doc(hidden))]
pub fn widget_style<S: WidgetStyle + Clone + 'static>(
&self,
id: crate::Id,
classes: &Classes,
) -> S {
// Fetch the state of the widget, as it was in the previous pass
let state = if let Some(response) = self.read_response(id) {
response.widget_state()
} else {
// We don't know the state of the widget yet, so we would style it wrong.
// It will be styled correctly on next frame.
self.ctx().request_repaint();
WidgetState::default()
};
self.get_widget_style::<S>(&StyleArgs {
classes,
state,
style: self.style(),
stack: self.stack(),
ctx: self,
})
}
}

View File

@@ -0,0 +1,17 @@
use core::any::TypeId;
use crate::widget_style::StyleArgs;
/// A Theme plugin that implement a style computation for a defined `WidgetStyle`
pub trait StyleProvider<S> {
/// The style according to the classes and state of the widget
fn style(&mut self, modifiers: &StyleArgs<'_>) -> S;
/// Used to tell different themes apart
fn type_id(&self) -> TypeId
where
Self: 'static,
{
TypeId::of::<Self>()
}
}

View File

@@ -0,0 +1,96 @@
use std::sync::Arc;
use epaint::mutex::Mutex;
use crate::{
Id,
theme::{StyleProvider, default_style::DefaultStyle},
util::IdTypeMap,
widget_style::{
BaseStyle, ButtonStyle, CheckboxStyle, LabelStyle, SeparatorStyle, WidgetStyle,
},
};
/// The registry of [`StyleProvider`]s, one per [`WidgetStyle`] type.
///
/// Each widget asks this registry for the provider of its style type
/// (e.g. [`ButtonStyle`]), and that provider computes the final style from the
/// widget's classes and state.
///
/// A default provider is registered for every built-in style; register your
/// own with [`Context::add_widget_theme`](crate::Context::add_widget_theme) or
/// [`Context::replace_widget_theme`](crate::Context::replace_widget_theme).
pub struct Themes {
themes: IdTypeMap,
}
type ThemeWrap<S> = Arc<Mutex<Box<dyn StyleProvider<S> + Send + Sync>>>;
impl Default for Themes {
/// Register the default egui theme
fn default() -> Self {
let mut themes = IdTypeMap::default();
themes.insert_temp::<ThemeWrap<BaseStyle>>(
Id::NULL,
Arc::new(Mutex::new(Box::new(DefaultStyle))),
);
themes.insert_temp::<ThemeWrap<ButtonStyle>>(
Id::NULL,
Arc::new(Mutex::new(Box::new(DefaultStyle))),
);
themes.insert_temp::<ThemeWrap<SeparatorStyle>>(
Id::NULL,
Arc::new(Mutex::new(Box::new(DefaultStyle))),
);
themes.insert_temp::<ThemeWrap<CheckboxStyle>>(
Id::NULL,
Arc::new(Mutex::new(Box::new(DefaultStyle))),
);
themes.insert_temp::<ThemeWrap<LabelStyle>>(
Id::NULL,
Arc::new(Mutex::new(Box::new(DefaultStyle))),
);
Self { themes }
}
}
impl Themes {
/// Register a [`StyleProvider`] for the specified widget [`WidgetStyle`] `S`
///
/// Existing themes are overwritten if `force` is `true` or the new theme differs.
pub(crate) fn register<S: WidgetStyle + 'static>(
&mut self,
theme: impl StyleProvider<S> + Send + Sync + 'static,
force: bool,
) {
if !force
&& self
.themes
.get_temp::<ThemeWrap<S>>(Id::NULL)
.is_some_and(|t| t.lock().type_id() == theme.type_id())
{
return;
}
self.themes
.insert_temp::<ThemeWrap<S>>(Id::NULL, Arc::new(Mutex::new(Box::new(theme))));
}
/// Fetch the style of the current theme
pub fn get<S: WidgetStyle + 'static>(&self) -> ThemeWrap<S> {
let v = self.themes.get_temp::<ThemeWrap<S>>(Id::NULL);
v.unwrap_or_else(|| {
panic!(
"A style should be set for {:?}",
core::any::type_name::<S>()
)
})
}
}

View File

@@ -1,318 +0,0 @@
use std::{borrow::Cow, fmt};
use emath::Vec2;
use epaint::{Color32, FontId, Shadow, Stroke, text::TextWrapMode};
use smallvec::SmallVec;
use crate::{
Frame, Response, Style, TextBuffer as _, TextStyle,
style::{WidgetVisuals, Widgets},
};
/// General text style
pub struct TextVisuals {
/// Font used
pub font_id: FontId,
/// Font color
pub color: Color32,
/// Text decoration
pub underline: Stroke,
pub strikethrough: Stroke,
}
/// General widget style
pub struct WidgetStyle {
pub frame: Frame,
pub text: TextVisuals,
pub stroke: Stroke,
}
/// Dedicated button style
pub struct ButtonStyle {
pub frame: Frame,
pub text_style: TextVisuals,
}
/// Dedicated checkbox style
pub struct CheckboxStyle {
/// Frame around
pub frame: Frame,
/// Text next to it
pub text_style: TextVisuals,
/// Checkbox size
pub checkbox_size: f32,
/// Checkmark size
pub check_size: f32,
/// Frame of the checkbox itself
pub checkbox_frame: Frame,
/// Checkmark stroke
pub check_stroke: Stroke,
}
/// Dedicated label style
pub struct LabelStyle {
/// Frame around
pub frame: Frame,
/// Text style
pub text: TextVisuals,
/// Wrap mode used
pub wrap_mode: TextWrapMode,
}
/// Dedicated separator style
pub struct SeparatorStyle {
/// How much space is allocated in the layout direction
pub spacing: f32,
/// How to paint it
pub stroke: Stroke,
}
/// The different state of a widget can be
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub enum WidgetState {
Noninteractive,
#[default]
Inactive,
Hovered,
Active,
}
impl Widgets {
/// The widget visuals according to the state
pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
match state {
WidgetState::Noninteractive => &self.noninteractive,
WidgetState::Inactive => &self.inactive,
WidgetState::Hovered => &self.hovered,
WidgetState::Active => &self.active,
}
}
}
impl Response {
pub fn widget_state(&self) -> WidgetState {
if !self.sense.interactive() {
WidgetState::Noninteractive
} else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() {
WidgetState::Active
} else if self.hovered() || self.highlighted() {
WidgetState::Hovered
} else {
WidgetState::Inactive
}
}
}
impl Style {
/// The general widget style. The style is computed according to the classes and state of the widget.
pub fn widget_style(&self, _classes: &Classes, state: WidgetState) -> WidgetStyle {
let visuals = self.visuals.widgets.state(state);
let font_id = self.override_font_id.clone();
WidgetStyle {
frame: Frame {
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
corner_radius: visuals.corner_radius,
inner_margin: self.spacing.button_padding.into(),
..Default::default()
},
stroke: visuals.fg_stroke,
text: TextVisuals {
color: self
.visuals
.override_text_color
.unwrap_or_else(|| visuals.text_color()),
font_id: font_id.unwrap_or_else(|| TextStyle::Body.resolve(self)),
strikethrough: Stroke::NONE,
underline: Stroke::NONE,
},
}
}
/// The dedicated button style. The style is computed according to the classes and state of the widget.
/// It depend on the general widget style.
pub fn button_style(&self, classes: &Classes, state: WidgetState) -> ButtonStyle {
let mut visuals = *self.visuals.widgets.state(state);
let mut ws = self.widget_style(classes, state);
if classes.has(SELECTED_CLASS) {
visuals.weak_bg_fill = self.visuals.selection.bg_fill;
visuals.bg_fill = self.visuals.selection.bg_fill;
visuals.fg_stroke = self.visuals.selection.stroke;
ws.text.color = self.visuals.selection.stroke.color;
}
ButtonStyle {
frame: Frame {
fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke,
corner_radius: visuals.corner_radius,
outer_margin: (-Vec2::splat(visuals.expansion)).into(),
inner_margin: (self.spacing.button_padding + Vec2::splat(visuals.expansion)
- Vec2::splat(visuals.bg_stroke.width))
.into(),
..Default::default()
},
text_style: ws.text,
}
}
/// The dedicated checkbox style. The style is computed according to the classes and state of the widget.
/// It depend on the general widget style.
pub fn checkbox_style(&self, classes: &Classes, state: WidgetState) -> CheckboxStyle {
let visuals = self.visuals.widgets.state(state);
let ws = self.widget_style(classes, state);
CheckboxStyle {
frame: Frame::new(),
checkbox_size: self.spacing.icon_width,
check_size: self.spacing.icon_width_inner,
checkbox_frame: Frame {
fill: visuals.bg_fill,
corner_radius: visuals.corner_radius,
stroke: visuals.bg_stroke,
..Default::default()
},
text_style: ws.text,
check_stroke: ws.stroke,
}
}
/// The dedicated label style. The style is computed according to the classes and state of the widget.
/// It depend on the general widget style.
pub fn label_style(&self, classes: &Classes, state: WidgetState) -> LabelStyle {
let ws = self.widget_style(classes, state);
LabelStyle {
frame: Frame {
fill: ws.frame.fill,
inner_margin: 0.0.into(),
outer_margin: 0.0.into(),
stroke: Stroke::NONE,
shadow: Shadow::NONE,
corner_radius: 0.into(),
},
text: ws.text,
wrap_mode: TextWrapMode::Wrap,
}
}
/// The dedicated separator style. The style is computed according to the classes and state of the widget.
/// It depend on the general widget style.
pub fn separator_style(&self, _classes: &Classes, _state: WidgetState) -> SeparatorStyle {
let visuals = self.visuals.noninteractive();
SeparatorStyle {
spacing: 6.0,
stroke: visuals.bg_stroke,
}
}
}
/// The root class is a special class present on every top-level [`crate::Ui`].
pub const ROOT_CLASS: &str = "root";
/// The selected class is a special class present on selected [`crate::Button`].
pub const SELECTED_CLASS: &str = "selected";
/// A class is a static string identifier.
pub type ClassName = Cow<'static, str>;
/// Classes are string identifier that can be set on widget/Ui.
///
/// This can be used by styling engine to compute a different style
/// based on the set of classes present on the widget/Ui.
#[derive(Debug, Default, Clone)]
pub struct Classes {
classes: SmallVec<[ClassName; 5]>,
}
impl Classes {
/// Add a class to the list if the condition is true
#[inline]
fn add_if(&mut self, class: impl Into<ClassName>, condition: bool) {
if condition {
self.classes.push(class.into());
}
}
}
impl HasClasses for Classes {
fn classes(&self) -> &Classes {
self
}
fn classes_mut(&mut self) -> &mut Classes {
self
}
}
impl core::fmt::Display for Classes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.classes.iter().for_each(|class| {
let _ = f.write_str(class.as_str());
});
f.write_str("")
}
}
/// Any widgets supporting [`Classes`] must implement this trait
pub trait HasClasses {
fn classes(&self) -> &Classes;
fn classes_mut(&mut self) -> &mut Classes;
/// Add the given class by consuming [`self`]
#[inline]
fn with_class(mut self, class: impl Into<ClassName>) -> Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), true);
self
}
/// Add the given class by consuming [`self`] if the condition is true
#[inline]
fn with_class_if(mut self, class: impl Into<ClassName>, condition: bool) -> Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), condition);
self
}
/// Add the given class in-place
#[inline]
fn add_class(&mut self, class: impl Into<ClassName>) -> &mut Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), true);
self
}
/// Add the given class in-place if the condition is true
#[inline]
fn add_class_if(&mut self, class: impl Into<ClassName>, condition: bool) -> &mut Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), condition);
self
}
/// True if the class is present
fn has(&self, class: impl Into<ClassName>) -> bool {
self.classes().classes.contains(&class.into())
}
}

View File

@@ -0,0 +1,109 @@
use std::{borrow::Cow, fmt};
use smallvec::SmallVec;
use crate::TextBuffer as _;
/// The root class is a special class present on every top-level [`crate::Ui`].
pub const ROOT_CLASS: &str = "root";
/// The selected class is a special class present on selected [`crate::Button`].
pub const SELECTED_CLASS: &str = "selected";
/// A class is a static string identifier.
pub type ClassName = Cow<'static, str>;
/// Classes are string identifier that can be set on widget/Ui.
///
/// This can be used by styling engine to compute a different style
/// based on the set of classes present on the widget/Ui.
#[derive(Debug, Default, Clone, Hash)]
pub struct Classes {
classes: SmallVec<[ClassName; 5]>,
}
impl Classes {
/// Add a class to the list if the condition is true
#[inline]
fn add_if(&mut self, class: impl Into<ClassName>, condition: bool) {
if condition {
self.classes.push(class.into());
}
}
}
impl HasClasses for Classes {
fn classes(&self) -> &Classes {
self
}
fn classes_mut(&mut self) -> &mut Classes {
self
}
}
impl core::fmt::Display for Classes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.classes.iter().for_each(|class| {
let _ = f.write_str(class.as_str());
});
f.write_str("")
}
}
/// Any widgets supporting [`Classes`] must implement this trait
pub trait HasClasses {
fn classes(&self) -> &Classes;
fn classes_mut(&mut self) -> &mut Classes;
/// Add the given class by consuming `self`
#[inline]
fn with_class(mut self, class: impl Into<ClassName>) -> Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), true);
self
}
/// Add the given class by consuming `self` if the condition is true
#[inline]
fn with_class_if(mut self, class: impl Into<ClassName>, condition: bool) -> Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), condition);
self
}
/// Add the given class in-place
#[inline]
fn add_class(&mut self, class: impl Into<ClassName>) -> &mut Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), true);
self
}
/// Add the given class in-place if the condition is true
#[inline]
fn add_class_if(&mut self, class: impl Into<ClassName>, condition: bool) -> &mut Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), condition);
self
}
/// True if the class is present
fn has(&self, class: impl Into<ClassName>) -> bool {
self.classes().classes.contains(&class.into())
}
/// The list of class
fn as_slice(&self) -> &[ClassName] {
&self.classes().classes
}
}

View File

@@ -0,0 +1,149 @@
// This module is only public with the `experimental_theme` feature,
// so without it a lot of it looks unused:
#![cfg_attr(not(feature = "experimental"), allow(dead_code, unused_imports))]
mod classes;
pub use self::classes::{ClassName, Classes, HasClasses, ROOT_CLASS, SELECTED_CLASS};
use core::fmt::Debug;
use epaint::{Color32, FontId, Stroke, text::TextWrapMode};
use crate::{
Context, Frame, Response, Style, UiStack,
style::{WidgetVisuals, Widgets},
};
/// Each dedicated style must implement this trait to be used in the theme plugin system
pub trait WidgetStyle: Debug + Clone + Send + Sync + core::any::Any + 'static {}
/// General text style
#[derive(Debug, Clone)]
pub struct TextVisuals {
/// Font used
pub font_id: FontId,
/// Font color
pub color: Color32,
/// Text decoration
pub underline: Stroke,
pub strikethrough: Stroke,
}
/// General widget style
#[derive(Debug, Clone)]
pub struct BaseStyle {
pub frame: Frame,
pub text: TextVisuals,
pub stroke: Stroke,
}
impl WidgetStyle for BaseStyle {}
/// Dedicated button style
#[derive(Debug, Clone)]
pub struct ButtonStyle {
pub frame: Frame,
pub text_style: TextVisuals,
}
impl WidgetStyle for ButtonStyle {}
/// Dedicated checkbox style
#[derive(Debug, Clone)]
pub struct CheckboxStyle {
/// Frame around
pub frame: Frame,
/// Text next to it
pub text_style: TextVisuals,
/// Checkbox size
pub checkbox_size: f32,
/// Checkmark size
pub check_size: f32,
/// Frame of the checkbox itself
pub checkbox_frame: Frame,
/// Checkmark stroke
pub check_stroke: Stroke,
}
impl WidgetStyle for CheckboxStyle {}
/// Dedicated label style
#[derive(Debug, Clone)]
pub struct LabelStyle {
/// Frame around
pub frame: Frame,
/// Text style
pub text: TextVisuals,
/// Wrap mode used
pub wrap_mode: TextWrapMode,
}
impl WidgetStyle for LabelStyle {}
/// Dedicated separator style
#[derive(Debug, Clone)]
pub struct SeparatorStyle {
/// How much space is allocated in the layout direction
pub spacing: f32,
/// How to paint it
pub stroke: Stroke,
}
impl WidgetStyle for SeparatorStyle {}
/// The different state of a widget can be
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum WidgetState {
Noninteractive,
#[default]
Inactive,
Hovered,
Active,
}
impl Widgets {
/// The widget visuals according to the state
pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
match state {
WidgetState::Noninteractive => &self.noninteractive,
WidgetState::Inactive => &self.inactive,
WidgetState::Hovered => &self.hovered,
WidgetState::Active => &self.active,
}
}
}
impl Response {
pub fn widget_state(&self) -> WidgetState {
if !self.sense.interactive() {
WidgetState::Noninteractive
} else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() {
WidgetState::Active
} else if self.hovered() || self.highlighted() {
WidgetState::Hovered
} else {
WidgetState::Inactive
}
}
}
pub struct StyleArgs<'a> {
pub classes: &'a Classes,
pub state: WidgetState,
pub stack: &'a UiStack,
pub style: &'a Style,
pub ctx: &'a Context,
}

View File

@@ -328,7 +328,7 @@ impl<'a> Button<'a> {
classes.add_class_if(SELECTED_CLASS, selected.unwrap_or(false));
let ButtonStyle { frame, text_style } = ui.style().button_style(&classes, state);
let ButtonStyle { frame, text_style } = ui.widget_style(id, &classes);
let mut button_padding = if has_frame_margin {
frame.inner_margin

View File

@@ -70,9 +70,6 @@ impl Widget for Checkbox<'_> {
// Get the widget style by reading the response from the previous pass
let id = ui.next_auto_id();
let response: Option<Response> = ui.ctx().read_response(id);
let state = response.map(|r| r.widget_state()).unwrap_or_default();
let CheckboxStyle {
check_size,
checkbox_frame,
@@ -80,7 +77,7 @@ impl Widget for Checkbox<'_> {
frame,
check_stroke,
text_style,
} = ui.style().checkbox_style(&classes, state);
} = ui.widget_style(id, &classes);
let mut min_size = Vec2::splat(ui.spacing().interact_size.y);
min_size.y = min_size.y.at_least(checkbox_size);

View File

@@ -6,9 +6,11 @@ use crate::{
WidgetInfo, WidgetType, epaint, lerp, remap_clamp,
};
use epaint::{
Mesh, Rect, Shape, Stroke, StrokeKind, Vec2,
ColorImage, Rect, RectShape, RoundedRect, Shape, Stroke, StrokeKind, Vec2,
ecolor::{Color32, Hsva, HsvaGamma, Rgba},
pos2, vec2,
pos2,
textures::TextureOptions,
vec2,
};
fn contrast_color(color: impl Into<Rgba>) -> Color32 {
@@ -19,38 +21,63 @@ fn contrast_color(color: impl Into<Rgba>) -> Color32 {
}
}
/// Number of vertices per dimension in the color sliders.
/// Resolution of the color slider gradients: the textures have `N + 1` texels per dimension.
/// We need at least 6 for hues, and more for smooth 2D areas.
/// Should always be a multiple of 6 to hit the peak hues in HSV/HSL (every 60°).
const N: u32 = 6 * 6;
fn background_checkers(painter: &Painter, rect: Rect) {
let rect = rect.shrink(0.5); // Small hack to avoid the checkers from peeking through the sides
if !rect.is_positive() {
fn background_checkers(painter: &Painter, bounds: RoundedRect) {
// Shrink slightly, so the dark checkers don't peek through
// the antialiased edge of the color painted on top:
let pixel_width = 1.0 / painter.ctx().pixels_per_point();
let (rect, corner_radius) = bounds.shrink(pixel_width).into_parts();
if !rect.is_positive() || !rect.is_finite() {
return;
}
let dark_color = Color32::from_gray(32);
let bright_color = Color32::from_gray(128);
let checker_size = Vec2::splat(rect.height() / 2.0);
let n = (rect.width() / checker_size.x).round() as u32;
// A single repeating 2x2 checker tile, stretched so that
// each checker is half the rect height, like before:
let image = ColorImage::new(
[2, 2],
vec![bright_color, dark_color, dark_color, bright_color],
);
let texture = painter.ctx().load_texture_cached(
"color_picker_checkers",
Id::new("color_picker_checkers"),
image,
TextureOptions::NEAREST_REPEAT,
);
let mut mesh = Mesh::default();
mesh.add_colored_rect(rect, dark_color);
// One tile (one uv unit) covers 2x2 checkers, i.e. the full rect height:
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(rect.width() / rect.height(), 1.0));
painter
.add(RectShape::filled(rect, corner_radius, Color32::WHITE).with_texture(texture.id(), uv));
}
let mut top = true;
for i in 0..n {
let x = lerp(rect.left()..=rect.right(), i as f32 / (n as f32));
let small_rect = if top {
Rect::from_min_size(pos2(x, rect.top()), checker_size)
} else {
Rect::from_min_size(pos2(x, rect.center().y), checker_size)
};
mesh.add_colored_rect(small_rect, bright_color);
top = !top;
}
painter.add(Shape::mesh(mesh));
/// Paint a gradient image over the given rounded rect, with an outline.
fn paint_gradient(ui: &Ui, id: Id, bounds: RoundedRect, outline: Stroke, image: ColorImage) {
let (rect, corner_radius) = bounds.into_parts();
let [width, height] = image.size;
let texture = ui
.ctx()
.load_texture_cached("color_gradient", id, image, TextureOptions::LINEAR);
// Inset the uv by half a texel, so the edges sample the exact edge colors:
let inset = vec2(0.5 / width as f32, 0.5 / height as f32);
let uv = Rect::from_min_max(inset.to_pos2(), pos2(1.0 - inset.x, 1.0 - inset.y));
ui.painter()
.add(RectShape::filled(rect, corner_radius, Color32::WHITE).with_texture(texture.id(), uv));
// The outline must be a separate shape:
// a textured `RectShape` cannot have a stroke,
// since the stroke vertices would sample the same texture.
ui.painter()
.rect_stroke(rect, corner_radius, outline, StrokeKind::Inside);
}
/// Show a color with background checkers to demonstrate transparency (if any).
@@ -72,7 +99,7 @@ pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) {
painter.rect_filled(rect, 0.0, color);
} else {
// Transparent: how both the transparent and opaque versions of the color
background_checkers(painter, rect);
background_checkers(painter, rect.into());
if color == Color32::TRANSPARENT {
// There is no opaque version, so just show the background checkers
@@ -89,21 +116,35 @@ pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) {
fn show_srgba_unmultiplied(ui: &mut Ui, srgba: [u8; 4], desired_size: Vec2) -> Response {
let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover());
if ui.is_rect_visible(rect) {
show_srgba_unmultiplied_at(ui.painter(), srgba, rect);
let corner_radius = ui.visuals().widgets.noninteractive.corner_radius;
show_srgba_unmultiplied_at(ui.painter(), srgba, RoundedRect::new(rect, corner_radius));
}
response
}
/// Show a color with background checkers to demonstrate transparency (if any).
fn show_srgba_unmultiplied_at(painter: &Painter, [r, g, b, a]: [u8; 4], rect: Rect) {
fn show_srgba_unmultiplied_at(painter: &Painter, [r, g, b, a]: [u8; 4], bounds: RoundedRect) {
let (rect, corner_radius) = bounds.into_parts();
if a == 255 {
painter.rect_filled(rect, 0.0, Color32::from_rgb(r, g, b));
painter.rect_filled(rect, corner_radius, Color32::from_rgb(r, g, b));
} else {
background_checkers(painter, rect);
let left = Rect::from_min_max(rect.left_top(), rect.center_bottom());
let right = Rect::from_min_max(rect.center_top(), rect.right_bottom());
painter.rect_filled(left, 0.0, Color32::from_rgba_unmultiplied(r, g, b, a));
painter.rect_filled(right, 0.0, Color32::from_rgb(r, g, b));
// Clamp to what the half-width rects can express,
// so the checkers and both halves all round their corners the same:
let corner_radius = corner_radius.at_most(0.5 * left.size().min_elem());
background_checkers(painter, RoundedRect::new(rect, corner_radius));
let left_corner_radius = corner_radius.with_east(0.0);
let right_corner_radius = corner_radius.with_west(0.0);
painter.rect_filled(
left,
left_corner_radius,
Color32::from_rgba_unmultiplied(r, g, b, a),
);
painter.rect_filled(right, right_corner_radius, Color32::from_rgb(r, g, b));
}
}
@@ -121,9 +162,15 @@ fn color_button(ui: &mut Ui, srgba: [u8; 4], open: bool) -> Response {
let rect = rect.expand(visuals.expansion);
let stroke_width = 1.0;
show_srgba_unmultiplied_at(ui.painter(), srgba, rect.shrink(stroke_width));
let corner_radius = visuals.corner_radius;
show_srgba_unmultiplied_at(
ui.painter(),
srgba,
// Shrink both the rect and the corner radius,
// so the fill arcs stay concentric with the inside stroke:
RoundedRect::new(rect, corner_radius).shrink(stroke_width),
);
let corner_radius = visuals.corner_radius.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding
ui.painter().rect_stroke(
rect,
corner_radius,
@@ -136,8 +183,6 @@ fn color_button(ui: &mut Ui, srgba: [u8; 4], open: bool) -> Response {
}
fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color32) -> Response {
#![expect(clippy::identity_op)]
let desired_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
@@ -147,29 +192,25 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
let corner_radius = visuals.corner_radius;
let bounds = RoundedRect::new(rect, corner_radius);
background_checkers(ui.painter(), rect); // for alpha:
background_checkers(ui.painter(), bounds); // for alpha:
{
// fill color:
let mut mesh = Mesh::default();
for i in 0..=N {
let t = i as f32 / (N as f32);
let color = color_at(t);
let x = lerp(rect.left()..=rect.right(), t);
mesh.colored_vertex(pos2(x, rect.top()), color);
mesh.colored_vertex(pos2(x, rect.bottom()), color);
if i < N {
mesh.add_triangle(2 * i + 0, 2 * i + 1, 2 * i + 2);
mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
}
}
ui.painter().add(Shape::mesh(mesh));
// fill color gradient:
let width = N as usize + 1;
let pixels = (0..width).map(|i| color_at(i as f32 / N as f32)).collect();
let image = ColorImage::new([width, 1], pixels);
paint_gradient(
ui,
response.id.with("gradient"),
bounds,
visuals.bg_stroke,
image,
);
}
ui.painter()
.rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline
{
// Show where the slider is at:
let x = lerp(rect.left()..=rect.right(), *value);
@@ -215,30 +256,28 @@ fn color_slider_2d(
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
let mut mesh = Mesh::default();
let corner_radius = visuals.corner_radius;
for xi in 0..=N {
{
// fill color gradient:
let width = N as usize + 1;
let mut pixels = Vec::with_capacity(width * width);
for yi in 0..=N {
let xt = xi as f32 / (N as f32);
let yt = yi as f32 / (N as f32);
let color = color_at(xt, yt);
let x = lerp(rect.left()..=rect.right(), xt);
let y = lerp(rect.bottom()..=rect.top(), yt);
mesh.colored_vertex(pos2(x, y), color);
if xi < N && yi < N {
let x_offset = 1;
let y_offset = N + 1;
let tl = yi * y_offset + xi;
mesh.add_triangle(tl, tl + x_offset, tl + y_offset);
mesh.add_triangle(tl + x_offset, tl + y_offset, tl + y_offset + x_offset);
let yt = 1.0 - yi as f32 / (N as f32); // texel rows go from top to bottom
for xi in 0..=N {
let xt = xi as f32 / (N as f32);
pixels.push(color_at(xt, yt));
}
}
let image = ColorImage::new([width, width], pixels);
paint_gradient(
ui,
response.id.with("gradient"),
RoundedRect::new(rect, corner_radius),
visuals.bg_stroke,
image,
);
}
ui.painter().add(Shape::mesh(mesh)); // fill
ui.painter()
.rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline
// Show where the slider is at:
let x = lerp(rect.left()..=rect.right(), *x_value);

View File

@@ -101,12 +101,10 @@ impl Widget for Separator {
// Get the widget style by reading the response from the previous pass
let id = ui.next_auto_id();
let response: Option<Response> = ui.ctx().read_response(id);
let state = response.map(|r| r.widget_state()).unwrap_or_default();
let SeparatorStyle {
spacing: spacing_style,
stroke,
} = ui.style().separator_style(&classes, state);
} = ui.widget_style(id, &classes);
// override the spacing if not set
let spacing = spacing.unwrap_or(spacing_style);

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dda3ad81551d0001bb9d3614a266010e44c8c18700964d8734c3e825672d9383
size 334759
oid sha256:f5290f645746e693fa7c9c7cb2985b0ec4a2522d52629e2cf583e683a3d67b58
size 334814

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9035687ecd3aae0d8639000f091592d69acec0e73e7ba17e04c9794c5321cb1e
size 92771
oid sha256:5ac1089b1502496408e932025892e9f587ae62861e2d3cf2620bf5c5a09aa265
size 92929

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:418f9bc7eb13dd32ce2bba8e4b8003b095dd00aa8c30f1640ea0f94f87a085eb
size 168430
oid sha256:dde8e504560d658fbb3eaee0dc12bbbd5c930841bc5637bc60274773d8a50e2d
size 168735

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d278d0d879e8cb44c4c2e65bdee4441616abcf3507adc37959ccf042991b31c
size 98780
oid sha256:24fc466f6470761a5064a590929fa3a0d95741ed691fc257415eee349bc528e9
size 101394

View File

@@ -71,13 +71,13 @@ impl crate::Demo for WindowOptions {
let mut window = egui::Window::new(title)
.id(egui::Id::new("demo_window_options")) // required since we change the title
.resizable(resizable)
.constrain_to(ui.available_rect_before_wrap())
.constrain(constrain)
.collapsible(collapsible)
.movable(movable)
.title_bar(title_bar)
.drag_area(drag_area)
.scroll(scroll2)
.constrain_to(ui.available_rect_before_wrap())
.enabled(enabled);
if closable {
window = window.open(open);

View File

@@ -0,0 +1,17 @@
use egui::Color32;
use egui::color_picker::{Alpha, color_picker_color32};
use egui_kittest::Harness;
#[test]
fn color_picker() {
let mut color = Color32::from_rgba_unmultiplied(130, 130, 130, 45);
let mut harness = Harness::builder()
.with_pixels_per_point(2.0)
.build_ui(move |ui| {
ui.spacing_mut().slider_width = 275.0;
color_picker_color32(ui, &mut color, Alpha::OnlyBlend);
});
harness.run();
harness.fit_contents();
harness.snapshot("color_picker");
}

View File

@@ -1,6 +1,41 @@
use egui::{Color32, accesskit::Role};
use egui_kittest::{Harness, kittest::Queryable as _};
/// Textures with [`egui::TextureOptions::NEAREST`] should render crisp,
/// also with kittest's predictable texture filtering.
#[test]
fn test_nearest_texture_filtering() {
let mut texture: Option<egui::TextureHandle> = None;
let mut harness = Harness::builder()
.with_size(egui::Vec2::new(80.0, 48.0))
.build_ui(move |ui| {
let texture = texture.get_or_insert_with(|| {
let pixels = [
Color32::BLACK,
Color32::WHITE,
Color32::BLACK,
Color32::WHITE,
Color32::WHITE,
Color32::BLACK,
Color32::WHITE,
Color32::BLACK,
];
let image = egui::ColorImage::new([4, 2], pixels.to_vec());
ui.ctx()
.load_texture("checkerboard", image, egui::TextureOptions::NEAREST)
});
let rect = egui::Rect::from_min_size(egui::pos2(8.0, 8.0), egui::vec2(64.0, 32.0));
let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
ui.painter().add(
egui::epaint::RectShape::filled(rect, 0, Color32::WHITE)
.with_texture(texture.id(), uv),
);
});
harness.run();
harness.snapshot("nearest_texture_filtering");
}
#[test]
fn test_kerning() {
let mut results = egui_kittest::SnapshotResults::new();

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed0f9f3ec94ac3d75266343d11cae5842d72fbea3c78f7995b1b0ad5aa3cdcad
size 141933

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c777cd6b36219b92c88ebf5987f9239a5750d25be4a87a58841bcf6db259599
size 32422
oid sha256:1bb3195aa5eb1fa7da6f6bb52eec745704e10eb82efb68d5f328f9bfa09fc2d4
size 32538

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d941c979def6f30fd227c144d20c7f138f9c06c3338d563ac075c5e17e9b795
size 26911
oid sha256:b6351ddb148941f73a182e676a3be4ac5d7165c618324459d0061dbf93f213d9
size 27023

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:05527c073b1ee2f6a15052a9097d1ad515331ff32d572cf510086f9ebb7a7bbc
size 27253
oid sha256:3a4ac42a9b5e44074caa2ec450bd54d834c14182ad8929a5f0567b53bbe5359a
size 27426

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:73429ec71d1ae05518352f1a99304f55647dac362092c9cd5618f709b9ba0d11
size 99950
oid sha256:611b18575d2768d40b89f28427bd987d90393e886da2a2c4e09ea6503f85acdd
size 100303

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79ee28a9c0d8ef80d53560584312a33569f668e0a49f6ad5d277ffef507f2818
size 25116
oid sha256:68351659df756ce331a4eeb147be8d1b4fed39dbbb54a0d6f685997b54dc07a7
size 26327

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ff02fa99b92059d7b4f8286b8a2fd2e27a7f8f005328da97f0b9786aac9bbb17
size 99316
oid sha256:f7d190cbc76e61133a6a1d1c800c24fa5de7dba09e3ba89dff9dc5b71ab8e788
size 100543

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e2f29678c41e45bedc916dbe845f05c343bfad5528a4143c32cac6c3dee41e7
size 50512
oid sha256:a2b67e1e43d55c26ec2dfcf420ef7c5f009e3e5c501780ac59b3f1848728b833
size 50940

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:345ab9ac3586c3cb9d3b02d46c590a81c7bc31778a3b631874520e57b6694076
size 22994
oid sha256:7eacac965912ded40f68e8b3c6d91218e6688c12b383e64fabf0d92ea9c73d9e
size 24039

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:353d92d99e9c2350267a43bff7bb9100d3109c82f8d54f9f7a9e3a312ea0c4e6
size 35378
oid sha256:eba72ac8696d81552f5106b02437f0b524eb77e61ff1d8014cebce6483eaa784
size 35474

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:218cce13bd96c97aca6a529ba49baaeda5a126cf657e6af1788646cb845725d0
size 17686
oid sha256:e9d4349ba73e1d1191191ebd1918ed80e373036e13dee837400194f53b0b9abd
size 17899

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3edbe6debf700364949ef481ba1e8b2319db624ddc316570b6180636dcd88935
size 57262
oid sha256:294a43fb1f3117450cde67cb38c4684b90a7b0f69d9c6e7002c0f6d06c70d02c
size 58044

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:141271330d4cef517c4ec2c4c2c41306f8b5de386738381a0e4446fa89df7cdf
size 30450
oid sha256:65d1d15d78c871eb87224c44d2fe889cc0d7a33eb15aeb4852c10bdb02aa730d
size 30877

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be289c58296a7656c8550d25ff92b8cd0b03c5c9c527b9ca4a38acda8bbe3ac1
size 23807
oid sha256:80091cab95735142ca35100cc74f784aa92812eeaf2e6ef991389de3651b4cfa
size 24045

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc28830c402d3aa861a220cfc600c3ce2d7b8ca77307691b3f844a34f27afd1f
size 151376
oid sha256:e7777f85030bad9c28237cdcb77d0835eafd3a1c69603545860cd980c1a2cbbd
size 151843

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca08bb7cd0be0b5a9d10833e5c9b65e889f78a5a8056df69d6b0006c3a228227
size 66050
oid sha256:17c16934b22f0439d4a13a78d91a85aec7ff740aeff9f8feb9dda3b059c4d83b
size 67107

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97452108d7809775756f1a229644076f5b4c75a6a6d8b8edc082c2448ae35e94
size 60701
oid sha256:ccac475bb38e76dc044d34f0a48e73b61e9d4734d02105c5635e02bcdaa19c63
size 62104

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d27392f625445e82285c8eaeb532e11dcb02030f24c7a769356cb4c23d7c2067
size 41516
oid sha256:7bbfacbbc740729ce8729f4507c6cc155a0dcda51dd4442d3db332cf2f6fd9f1
size 42790

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90c4ea93f234aded298ddeeb98e2d6a0566f07efcdaf5c990e8a47a6533db516
size 446483
oid sha256:9ac3830efd9c5eb051ed6bf22bd90dd556883b29505096c3901f28f8673b6eba
size 446518

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4b069ad508fd3a518df3a39e1740d1786c4af4c3c4a479db04c00f701980efa8
size 48277
oid sha256:2718c333123388c8751f461382867b0640ce7a02c220db311c06ff634e6178c1
size 48620

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e92e72c651e0ad2c0fc38008d24d9ddf28a70e3b35b26898af78e74028e2c251
size 44357
oid sha256:a41a61cce2cda5fb9372324f915ffe2b47f68ead5e26b471ea29dfb5208695a7
size 44600

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a5f7bc46c7baf62b559458b3596e386d66bf2281b7c35e5fdb6c5c4a64569f2
size 44347
oid sha256:bff8f0531d5cf92e0afe857a04705599d30e77d526091b03296e381b45f02170
size 44488

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72e133c853ab933d37711665ce1278f8f23ce35b8a74a46559f269d0dfd0bf40
size 353

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce6e44aeb60b6cc2179e14c467edc2d570ea1adbce08511650fc733e39f7757c
size 590679
oid sha256:10d58fa20fe3cc06a98dc79adf6237652b87871d2dc394476ea524ac62f2750b
size 590735

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2edd6679d434df3407048472aaa95e410158f28e9a13c9bd88e3f8be95b08d9d
size 745771
oid sha256:1de8583bef3fb9e3530b4a09f498360d37d21c099dd44cd45ee99ccc332d7c1c
size 745855

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20cb796b59113853345c98e5238021a9c5c977c25689ee9201fdec1e4d98123d
size 973781
oid sha256:43c1f297b0d3e62483b05efe3eb7089a61944b2e348bc791f0503ccbf6c5ab4a
size 973927

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9658cc6b4dd2dd33356facc6b48cf69f9a86f976ac2dd4f16e362a2fd3f73827
size 1081864
oid sha256:17790adf9f5ba91aa00a22c44303422ed02e71a1ab7b5c1484e13e2a132bd639
size 1081792

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5d5941c8fa0456f8a92fce4888e7a2888945f5df7cfed867ef073c439c7ad9f
size 1131030
oid sha256:b434961f2ac616e2563aa2ff9e706158190dfcc2713de6fd7740290e603c8a1b
size 1131176

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:78ef6dba3dcf5e26a9289ce226165224c712c22f9b5dfe699b599c7d2531133b
size 1363589
oid sha256:de79f0a3dacdf5387faaeeb64f8939ff5912e362d8d649d6e25fd3dbda940325
size 1363775

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3daf9b7cbfb48f6d083126e58c605cb19f462509acc03843f0bca5c645945e23
size 45044
oid sha256:a62ed81b1693cd7eea2099c0840d82318d27e382f88ca1dd15ba9e2a59e3cf4b
size 46677

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de7db1296bdd127dfd827c6d1cc1a5a840391c3abb558be07faeb7e6612ed6e8
size 86830
oid sha256:d9139bb87beac6bdb91f0e48f5cabba61ca7ba5344a8fcefd094d2efb98e7056
size 88246

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4ef3a17791ca4a3e0209eec5d191c00c66e08fc4dd07fb4845490288c5bfbdb6
size 118889
oid sha256:5ad40e8b41882e3456569c8cb2e831d33fafa910989674e05b20568798528154
size 120579

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:465a3aa235ea69bce6650cf6e100ffc719bba67baa0cb13d7015a0fc54d53d9a
size 51376
oid sha256:a596437f8274a87525ce107f94d6c8394166c3aa26d06737f1abdc2ae894f77b
size 52790

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aac429243686d35096760eb1b7c6a2bcad1c1d00a3341a858a6c71aaff2cf128
size 54582
oid sha256:001c89ba338b51d48f1e471e17306e84e74b0f952a856b65532ffeacbf52f2cb
size 55982

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8afb610d9e86b324db22849457eb23419c4f3c240e5ee951a13ce8901003bca8
size 55053
oid sha256:bf299c3c52250b1a17411e3e1aef57a4293787c9a9777da6fda01bb6a0e387c1
size 56466

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:75ff881add9f2968d19d8bb1d39239de5d99105027e91af209dfa5b45e644d2c
size 35943
oid sha256:ea03f6b0e0071910cf2b4279549fb0347cd6a9f8d0334830ce4f34f5824a1ef9
size 37595

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1887d632d0994efec3dd7cecc41e3ad460c109baa1012d69e005cdc916e3cd24
size 35925
oid sha256:db4795c68fffb20f74d46664be9b7c4bfcaf941a8b3e27a7315f9525dd90d5c4
size 37340

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c0054dd1717833ae6f19726b58bffbf70bd4dc784b5f2774b194800a3adffed
size 64973
oid sha256:952df0869c400d8b1f5dc0a33cb7a8be1fa30d91929e57b7c33af0cfbc9cead4
size 66043

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9be7321d8184c3d06cfeb54410c1f839892c4f836767e9b1950e70306cde6c54
size 151090
oid sha256:066067e34d47aa8d2631e64b2ea37d94bae77bdfb1af9337f34f724e1ff14cb6
size 153589

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f07c25c053e9c4d5f7416cc00e101e13b7007b65c46ba13d0853d95ca43fddc4
size 59950
oid sha256:0d79861c18cbaf2f2ab738d0a5633f354c11a52b8a8e87c623e624ecdfcc6965
size 60813

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b6a9bc30b4d6ed100555ee553554848d20d93f25643e72eeef1826483ed307d9
size 145940
oid sha256:cde27ba5614f4bd5553d609b23e6dd78aea113c88443de42fa47aa85929c9455
size 148235

View File

@@ -6,7 +6,7 @@
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
This is a crate that adds some features on top top of [`egui`](https://github.com/emilk/egui). This crate is for experimental features, and features that require big dependencies that do not belong in `egui`.
This is a crate that adds some features on top of [`egui`](https://github.com/emilk/egui). This crate is for experimental features, and features that require big dependencies that do not belong in `egui`.
## Images
One thing `egui_extras` is commonly used for is to install image loaders for `egui`:

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:84e857521c20ea2458d9f9d724578e0495c06e75571c25104bf64a0be0be617d
size 8028
oid sha256:c32712afee568ce9dca8eeb6bd1dc0f072f9e9b9189ba0bbcd32f11317d73b98
size 8160

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57211d5ac8021223ed288b9fd6f651ed901ec91599ed67d54ac540f3a2037937
size 2205
oid sha256:1933058e8eb0568b6a42be5bcb4aae28a478daa9b62ccb0e72d33623f9161cb1
size 2246

View File

@@ -73,6 +73,7 @@ impl CornerRadiusF32 {
/// Same rounding on all four corners.
#[inline]
#[must_use]
pub const fn same(radius: f32) -> Self {
Self {
nw: radius,
@@ -90,6 +91,7 @@ impl CornerRadiusF32 {
/// Make sure each corner has a rounding of at least this.
#[inline]
#[must_use]
pub fn at_least(&self, min: f32) -> Self {
Self {
nw: self.nw.max(min),
@@ -101,6 +103,7 @@ impl CornerRadiusF32 {
/// Make sure each corner has a rounding of at most this.
#[inline]
#[must_use]
pub fn at_most(&self, max: f32) -> Self {
Self {
nw: self.nw.min(max),
@@ -109,79 +112,111 @@ impl CornerRadiusF32 {
se: self.se.min(max),
}
}
/// Set the rounding of the two east (right) corners.
#[inline]
#[must_use]
pub fn with_east(self, radius: f32) -> Self {
Self {
ne: radius,
se: radius,
..self
}
}
/// Set the rounding of the two north (top) corners.
#[inline]
#[must_use]
pub fn with_north(self, radius: f32) -> Self {
Self {
nw: radius,
ne: radius,
..self
}
}
/// Set the rounding of the two south (bottom) corners.
#[inline]
#[must_use]
pub fn with_south(self, radius: f32) -> Self {
Self {
sw: radius,
se: radius,
..self
}
}
/// Set the rounding of the two west (left) corners.
#[inline]
#[must_use]
pub fn with_west(self, radius: f32) -> Self {
Self {
nw: radius,
sw: radius,
..self
}
}
}
impl core::ops::Add for CornerRadiusF32 {
type Output = Self;
/// Saturates at zero: no corner radius will go negative.
#[inline]
fn add(self, rhs: Self) -> Self {
Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
nw: (self.nw + rhs.nw).max(0.0),
ne: (self.ne + rhs.ne).max(0.0),
sw: (self.sw + rhs.sw).max(0.0),
se: (self.se + rhs.se).max(0.0),
}
}
}
impl core::ops::AddAssign for CornerRadiusF32 {
/// Saturates at zero: no corner radius will go negative.
#[inline]
fn add_assign(&mut self, rhs: Self) {
*self = Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
};
*self = *self + rhs;
}
}
impl core::ops::AddAssign<f32> for CornerRadiusF32 {
/// Saturates at zero: no corner radius will go negative.
#[inline]
fn add_assign(&mut self, rhs: f32) {
*self = Self {
nw: self.nw + rhs,
ne: self.ne + rhs,
sw: self.sw + rhs,
se: self.se + rhs,
};
*self = *self + Self::same(rhs);
}
}
impl core::ops::Sub for CornerRadiusF32 {
type Output = Self;
/// Saturates at zero: no corner radius will go negative.
#[inline]
fn sub(self, rhs: Self) -> Self {
Self {
nw: self.nw - rhs.nw,
ne: self.ne - rhs.ne,
sw: self.sw - rhs.sw,
se: self.se - rhs.se,
nw: (self.nw - rhs.nw).max(0.0),
ne: (self.ne - rhs.ne).max(0.0),
sw: (self.sw - rhs.sw).max(0.0),
se: (self.se - rhs.se).max(0.0),
}
}
}
impl core::ops::SubAssign for CornerRadiusF32 {
/// Saturates at zero: no corner radius will go negative.
#[inline]
fn sub_assign(&mut self, rhs: Self) {
*self = Self {
nw: self.nw - rhs.nw,
ne: self.ne - rhs.ne,
sw: self.sw - rhs.sw,
se: self.se - rhs.se,
};
*self = *self - rhs;
}
}
impl core::ops::SubAssign<f32> for CornerRadiusF32 {
/// Saturates at zero: no corner radius will go negative.
#[inline]
fn sub_assign(&mut self, rhs: f32) {
*self = Self {
nw: self.nw - rhs,
ne: self.ne - rhs,
sw: self.sw - rhs,
se: self.se - rhs,
};
*self = *self - Self::same(rhs);
}
}
@@ -234,3 +269,26 @@ impl core::ops::MulAssign<f32> for CornerRadiusF32 {
};
}
}
#[cfg(test)]
mod tests {
use super::CornerRadiusF32;
#[test]
fn add_and_sub_saturate_at_zero() {
assert_eq!(
CornerRadiusF32::same(2.0) + CornerRadiusF32::same(-5.0),
CornerRadiusF32::ZERO
);
assert_eq!(
CornerRadiusF32::same(2.0) - CornerRadiusF32::same(5.0),
CornerRadiusF32::ZERO
);
let mut cr = CornerRadiusF32::same(1.0);
cr += -3.0;
assert_eq!(cr, CornerRadiusF32::ZERO);
cr -= -2.0;
assert_eq!(cr, CornerRadiusF32::same(2.0));
}
}

View File

@@ -33,6 +33,7 @@ mod margin;
mod margin_f32;
mod mesh;
pub mod mutex;
mod rounded_rect;
mod shadow;
pub mod shape_transform;
mod shapes;
@@ -56,6 +57,7 @@ pub use self::{
margin::Margin,
margin_f32::*,
mesh::{Mesh, Mesh16, Vertex},
rounded_rect::RoundedRect,
shadow::Shadow,
shapes::{
CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape,

View File

@@ -0,0 +1,159 @@
use emath::{Pos2, Rect, Vec2, vec2};
use crate::CornerRadiusF32;
/// A rectangle geometry with rounded corners.
///
/// Not a painting primitive. For that, see [`crate::RectShape`].
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct RoundedRect {
rect: Rect,
corner_radius: CornerRadiusF32,
}
impl RoundedRect {
/// The corner radius is clamped to half the size of the rectangle.
#[inline]
pub fn new(rect: Rect, corner_radius: impl Into<CornerRadiusF32>) -> Self {
let max_radius = 0.5 * rect.size().min_elem();
Self {
rect,
corner_radius: corner_radius.into().at_most(max_radius).at_least(0.0),
}
}
#[inline]
pub fn rect(&self) -> Rect {
self.rect
}
#[inline]
pub fn corner_radius(&self) -> CornerRadiusF32 {
self.corner_radius
}
/// Split into the rectangle and the corner radius.
#[inline]
pub fn into_parts(self) -> (Rect, CornerRadiusF32) {
let Self {
rect,
corner_radius,
} = self;
(rect, corner_radius)
}
/// Expand the rectangle and the corner radii by the given amount.
#[inline]
#[must_use]
pub fn expand(self, amount: f32) -> Self {
Self::new(
self.rect.expand(amount),
self.corner_radius + CornerRadiusF32::same(amount),
)
}
/// Shrink the rectangle and the corner radii by the given amount.
#[inline]
#[must_use]
pub fn shrink(self, amount: f32) -> Self {
self.expand(-amount)
}
/// Clamp the given position to lie within this rounded rectangle.
///
/// Positions in the corner regions are projected onto the corner arcs.
pub fn clamp_pos(&self, pos: Pos2) -> Pos2 {
let Self {
rect,
corner_radius,
} = *self;
let pos = rect.clamp(pos);
let corners = [
(corner_radius.nw, vec2(-1.0, -1.0)),
(corner_radius.ne, vec2(1.0, -1.0)),
(corner_radius.sw, vec2(-1.0, 1.0)),
(corner_radius.se, vec2(1.0, 1.0)),
];
for (radius, dir) in corners {
let arc_center = rect.center() + dir * (rect.size() / 2.0 - Vec2::splat(radius));
let offset = pos - arc_center;
if 0.0 < offset.x * dir.x && 0.0 < offset.y * dir.y && radius < offset.length() {
return arc_center + (radius / offset.length()) * offset;
}
}
pos
}
}
impl From<Rect> for RoundedRect {
#[inline]
fn from(rect: Rect) -> Self {
Self {
rect,
corner_radius: CornerRadiusF32::ZERO,
}
}
}
#[cfg(test)]
mod tests {
use emath::pos2;
use super::*;
#[test]
fn clamp_pos() {
let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0));
let rounded = RoundedRect::new(
rect,
CornerRadiusF32 {
nw: 10.0,
ne: 0.0,
sw: 0.0,
se: 20.0,
},
);
// Interior point is untouched:
assert_eq!(rounded.clamp_pos(pos2(50.0, 50.0)), pos2(50.0, 50.0));
// Sharp corner is untouched:
assert_eq!(rounded.clamp_pos(pos2(100.0, 0.0)), pos2(100.0, 0.0));
// Outside the rect is clamped to the edge:
assert_eq!(rounded.clamp_pos(pos2(-10.0, 50.0)), pos2(0.0, 50.0));
// Rounded corner is projected onto the arc:
let clamped = rounded.clamp_pos(pos2(0.0, 0.0));
let arc_center = pos2(10.0, 10.0);
assert!((clamped - arc_center).length() - 10.0 < 0.001);
let expected = 10.0 - 10.0 / core::f32::consts::SQRT_2;
assert!((clamped - pos2(expected, expected)).length() < 0.001);
// Point on the arc stays put:
assert_eq!(rounded.clamp_pos(pos2(10.0, 0.0)), pos2(10.0, 0.0));
}
#[test]
fn expand() {
let rect = Rect::from_min_max(pos2(10.0, 10.0), pos2(90.0, 90.0));
let expanded = RoundedRect::new(rect, 20.0).expand(10.0);
assert_eq!(
expanded.rect(),
Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0))
);
assert_eq!(expanded.corner_radius(), CornerRadiusF32::same(30.0));
}
#[test]
fn oversized_radius_is_clamped() {
// A radius larger than half the rect is clamped, like in the tessellator:
let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0));
assert_eq!(RoundedRect::new(rect, 200.0), RoundedRect::new(rect, 50.0));
assert_eq!(
RoundedRect::new(rect, 200.0).corner_radius(),
CornerRadiusF32::same(50.0)
);
}
}

View File

@@ -1,4 +1,4 @@
use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, Vec2};
use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, RoundedRect, Vec2};
/// The color and fuzziness of a fuzzy shape.
///
@@ -56,10 +56,12 @@ impl Shadow {
} = *self;
let [offset_x, offset_y] = offset;
let rect = rect
.translate(Vec2::new(offset_x as _, offset_y as _))
.expand(spread as _);
let corner_radius = corner_radius.into() + CornerRadius::from(spread);
let (rect, corner_radius) = RoundedRect::new(
rect.translate(Vec2::new(offset_x as _, offset_y as _)),
corner_radius.into(),
)
.expand(f32::from(spread))
.into_parts();
RectShape::filled(rect, corner_radius, color).with_blur_width(blur as _)
}

View File

@@ -11,8 +11,8 @@ use emath::{
use crate::{
CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape,
EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke,
StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke,
EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, RoundedRect, Shape,
Stroke, StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke,
texture_atlas::PreparedDisc,
};
@@ -536,18 +536,19 @@ impl Path {
pub mod path {
//! Helpers for constructing paths
use crate::CornerRadiusF32;
use emath::{Pos2, Rect, pos2};
use crate::{CornerRadiusF32, RoundedRect};
use emath::{Pos2, pos2};
/// overwrites existing points
pub fn rounded_rectangle(path: &mut Vec<Pos2>, rect: Rect, cr: CornerRadiusF32) {
pub fn rounded_rectangle(path: &mut Vec<Pos2>, rounded_rect: RoundedRect) {
path.clear();
// The corner radius is already clamped to half the rect size by `RoundedRect`:
let (rect, cr) = rounded_rect.into_parts();
let min = rect.min;
let max = rect.max;
let cr = clamp_corner_radius(cr, rect);
if cr == CornerRadiusF32::ZERO {
path.reserve(4);
path.push(pos2(min.x, min.y)); // left top
@@ -633,14 +634,6 @@ pub mod path {
path.extend(quadrant_vertices.iter().map(|&n| center + radius * n));
}
}
// Ensures the radius of each corner is within a valid range
fn clamp_corner_radius(cr: CornerRadiusF32, rect: Rect) -> CornerRadiusF32 {
let half_width = rect.width() * 0.5;
let half_height = rect.height() * 0.5;
let max_cr = half_width.min(half_height);
cr.at_most(max_cr).at_least(0.0)
}
}
// ----------------------------------------------------------------------------
@@ -1938,7 +1931,10 @@ impl Tessellator {
let path = &mut self.scratchpad_path;
path.clear();
path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius);
path::rounded_rectangle(
&mut self.scratchpad_points,
RoundedRect::new(rect, corner_radius),
);
// Apply rotation if angle is non-zero
if angle != 0.0 {