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:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
156
crates/egui/src/theme/default_style.rs
Normal file
156
crates/egui/src/theme/default_style.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
48
crates/egui/src/theme/mod.rs
Normal file
48
crates/egui/src/theme/mod.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
17
crates/egui/src/theme/style_provider.rs
Normal file
17
crates/egui/src/theme/style_provider.rs
Normal 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>()
|
||||
}
|
||||
}
|
||||
96
crates/egui/src/theme/themes.rs
Normal file
96
crates/egui/src/theme/themes.rs
Normal 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>()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
109
crates/egui/src/widget_style/classes.rs
Normal file
109
crates/egui/src/widget_style/classes.rs
Normal 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
|
||||
}
|
||||
}
|
||||
149
crates/egui/src/widget_style/mod.rs
Normal file
149
crates/egui/src/widget_style/mod.rs
Normal 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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dda3ad81551d0001bb9d3614a266010e44c8c18700964d8734c3e825672d9383
|
||||
size 334759
|
||||
oid sha256:f5290f645746e693fa7c9c7cb2985b0ec4a2522d52629e2cf583e683a3d67b58
|
||||
size 334814
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9035687ecd3aae0d8639000f091592d69acec0e73e7ba17e04c9794c5321cb1e
|
||||
size 92771
|
||||
oid sha256:5ac1089b1502496408e932025892e9f587ae62861e2d3cf2620bf5c5a09aa265
|
||||
size 92929
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:418f9bc7eb13dd32ce2bba8e4b8003b095dd00aa8c30f1640ea0f94f87a085eb
|
||||
size 168430
|
||||
oid sha256:dde8e504560d658fbb3eaee0dc12bbbd5c930841bc5637bc60274773d8a50e2d
|
||||
size 168735
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d278d0d879e8cb44c4c2e65bdee4441616abcf3507adc37959ccf042991b31c
|
||||
size 98780
|
||||
oid sha256:24fc466f6470761a5064a590929fa3a0d95741ed691fc257415eee349bc528e9
|
||||
size 101394
|
||||
|
||||
@@ -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);
|
||||
|
||||
17
crates/egui_demo_lib/tests/color_picker.rs
Normal file
17
crates/egui_demo_lib/tests/color_picker.rs
Normal 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");
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
3
crates/egui_demo_lib/tests/snapshots/color_picker.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/color_picker.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed0f9f3ec94ac3d75266343d11cae5842d72fbea3c78f7995b1b0ad5aa3cdcad
|
||||
size 141933
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c777cd6b36219b92c88ebf5987f9239a5750d25be4a87a58841bcf6db259599
|
||||
size 32422
|
||||
oid sha256:1bb3195aa5eb1fa7da6f6bb52eec745704e10eb82efb68d5f328f9bfa09fc2d4
|
||||
size 32538
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d941c979def6f30fd227c144d20c7f138f9c06c3338d563ac075c5e17e9b795
|
||||
size 26911
|
||||
oid sha256:b6351ddb148941f73a182e676a3be4ac5d7165c618324459d0061dbf93f213d9
|
||||
size 27023
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05527c073b1ee2f6a15052a9097d1ad515331ff32d572cf510086f9ebb7a7bbc
|
||||
size 27253
|
||||
oid sha256:3a4ac42a9b5e44074caa2ec450bd54d834c14182ad8929a5f0567b53bbe5359a
|
||||
size 27426
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:73429ec71d1ae05518352f1a99304f55647dac362092c9cd5618f709b9ba0d11
|
||||
size 99950
|
||||
oid sha256:611b18575d2768d40b89f28427bd987d90393e886da2a2c4e09ea6503f85acdd
|
||||
size 100303
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:79ee28a9c0d8ef80d53560584312a33569f668e0a49f6ad5d277ffef507f2818
|
||||
size 25116
|
||||
oid sha256:68351659df756ce331a4eeb147be8d1b4fed39dbbb54a0d6f685997b54dc07a7
|
||||
size 26327
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ff02fa99b92059d7b4f8286b8a2fd2e27a7f8f005328da97f0b9786aac9bbb17
|
||||
size 99316
|
||||
oid sha256:f7d190cbc76e61133a6a1d1c800c24fa5de7dba09e3ba89dff9dc5b71ab8e788
|
||||
size 100543
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3e2f29678c41e45bedc916dbe845f05c343bfad5528a4143c32cac6c3dee41e7
|
||||
size 50512
|
||||
oid sha256:a2b67e1e43d55c26ec2dfcf420ef7c5f009e3e5c501780ac59b3f1848728b833
|
||||
size 50940
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:345ab9ac3586c3cb9d3b02d46c590a81c7bc31778a3b631874520e57b6694076
|
||||
size 22994
|
||||
oid sha256:7eacac965912ded40f68e8b3c6d91218e6688c12b383e64fabf0d92ea9c73d9e
|
||||
size 24039
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:353d92d99e9c2350267a43bff7bb9100d3109c82f8d54f9f7a9e3a312ea0c4e6
|
||||
size 35378
|
||||
oid sha256:eba72ac8696d81552f5106b02437f0b524eb77e61ff1d8014cebce6483eaa784
|
||||
size 35474
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:218cce13bd96c97aca6a529ba49baaeda5a126cf657e6af1788646cb845725d0
|
||||
size 17686
|
||||
oid sha256:e9d4349ba73e1d1191191ebd1918ed80e373036e13dee837400194f53b0b9abd
|
||||
size 17899
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3edbe6debf700364949ef481ba1e8b2319db624ddc316570b6180636dcd88935
|
||||
size 57262
|
||||
oid sha256:294a43fb1f3117450cde67cb38c4684b90a7b0f69d9c6e7002c0f6d06c70d02c
|
||||
size 58044
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:141271330d4cef517c4ec2c4c2c41306f8b5de386738381a0e4446fa89df7cdf
|
||||
size 30450
|
||||
oid sha256:65d1d15d78c871eb87224c44d2fe889cc0d7a33eb15aeb4852c10bdb02aa730d
|
||||
size 30877
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:be289c58296a7656c8550d25ff92b8cd0b03c5c9c527b9ca4a38acda8bbe3ac1
|
||||
size 23807
|
||||
oid sha256:80091cab95735142ca35100cc74f784aa92812eeaf2e6ef991389de3651b4cfa
|
||||
size 24045
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fc28830c402d3aa861a220cfc600c3ce2d7b8ca77307691b3f844a34f27afd1f
|
||||
size 151376
|
||||
oid sha256:e7777f85030bad9c28237cdcb77d0835eafd3a1c69603545860cd980c1a2cbbd
|
||||
size 151843
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ca08bb7cd0be0b5a9d10833e5c9b65e889f78a5a8056df69d6b0006c3a228227
|
||||
size 66050
|
||||
oid sha256:17c16934b22f0439d4a13a78d91a85aec7ff740aeff9f8feb9dda3b059c4d83b
|
||||
size 67107
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:97452108d7809775756f1a229644076f5b4c75a6a6d8b8edc082c2448ae35e94
|
||||
size 60701
|
||||
oid sha256:ccac475bb38e76dc044d34f0a48e73b61e9d4734d02105c5635e02bcdaa19c63
|
||||
size 62104
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d27392f625445e82285c8eaeb532e11dcb02030f24c7a769356cb4c23d7c2067
|
||||
size 41516
|
||||
oid sha256:7bbfacbbc740729ce8729f4507c6cc155a0dcda51dd4442d3db332cf2f6fd9f1
|
||||
size 42790
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90c4ea93f234aded298ddeeb98e2d6a0566f07efcdaf5c990e8a47a6533db516
|
||||
size 446483
|
||||
oid sha256:9ac3830efd9c5eb051ed6bf22bd90dd556883b29505096c3901f28f8673b6eba
|
||||
size 446518
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b069ad508fd3a518df3a39e1740d1786c4af4c3c4a479db04c00f701980efa8
|
||||
size 48277
|
||||
oid sha256:2718c333123388c8751f461382867b0640ce7a02c220db311c06ff634e6178c1
|
||||
size 48620
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e92e72c651e0ad2c0fc38008d24d9ddf28a70e3b35b26898af78e74028e2c251
|
||||
size 44357
|
||||
oid sha256:a41a61cce2cda5fb9372324f915ffe2b47f68ead5e26b471ea29dfb5208695a7
|
||||
size 44600
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1a5f7bc46c7baf62b559458b3596e386d66bf2281b7c35e5fdb6c5c4a64569f2
|
||||
size 44347
|
||||
oid sha256:bff8f0531d5cf92e0afe857a04705599d30e77d526091b03296e381b45f02170
|
||||
size 44488
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72e133c853ab933d37711665ce1278f8f23ce35b8a74a46559f269d0dfd0bf40
|
||||
size 353
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce6e44aeb60b6cc2179e14c467edc2d570ea1adbce08511650fc733e39f7757c
|
||||
size 590679
|
||||
oid sha256:10d58fa20fe3cc06a98dc79adf6237652b87871d2dc394476ea524ac62f2750b
|
||||
size 590735
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2edd6679d434df3407048472aaa95e410158f28e9a13c9bd88e3f8be95b08d9d
|
||||
size 745771
|
||||
oid sha256:1de8583bef3fb9e3530b4a09f498360d37d21c099dd44cd45ee99ccc332d7c1c
|
||||
size 745855
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:20cb796b59113853345c98e5238021a9c5c977c25689ee9201fdec1e4d98123d
|
||||
size 973781
|
||||
oid sha256:43c1f297b0d3e62483b05efe3eb7089a61944b2e348bc791f0503ccbf6c5ab4a
|
||||
size 973927
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9658cc6b4dd2dd33356facc6b48cf69f9a86f976ac2dd4f16e362a2fd3f73827
|
||||
size 1081864
|
||||
oid sha256:17790adf9f5ba91aa00a22c44303422ed02e71a1ab7b5c1484e13e2a132bd639
|
||||
size 1081792
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b5d5941c8fa0456f8a92fce4888e7a2888945f5df7cfed867ef073c439c7ad9f
|
||||
size 1131030
|
||||
oid sha256:b434961f2ac616e2563aa2ff9e706158190dfcc2713de6fd7740290e603c8a1b
|
||||
size 1131176
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:78ef6dba3dcf5e26a9289ce226165224c712c22f9b5dfe699b599c7d2531133b
|
||||
size 1363589
|
||||
oid sha256:de79f0a3dacdf5387faaeeb64f8939ff5912e362d8d649d6e25fd3dbda940325
|
||||
size 1363775
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3daf9b7cbfb48f6d083126e58c605cb19f462509acc03843f0bca5c645945e23
|
||||
size 45044
|
||||
oid sha256:a62ed81b1693cd7eea2099c0840d82318d27e382f88ca1dd15ba9e2a59e3cf4b
|
||||
size 46677
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:de7db1296bdd127dfd827c6d1cc1a5a840391c3abb558be07faeb7e6612ed6e8
|
||||
size 86830
|
||||
oid sha256:d9139bb87beac6bdb91f0e48f5cabba61ca7ba5344a8fcefd094d2efb98e7056
|
||||
size 88246
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ef3a17791ca4a3e0209eec5d191c00c66e08fc4dd07fb4845490288c5bfbdb6
|
||||
size 118889
|
||||
oid sha256:5ad40e8b41882e3456569c8cb2e831d33fafa910989674e05b20568798528154
|
||||
size 120579
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:465a3aa235ea69bce6650cf6e100ffc719bba67baa0cb13d7015a0fc54d53d9a
|
||||
size 51376
|
||||
oid sha256:a596437f8274a87525ce107f94d6c8394166c3aa26d06737f1abdc2ae894f77b
|
||||
size 52790
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aac429243686d35096760eb1b7c6a2bcad1c1d00a3341a858a6c71aaff2cf128
|
||||
size 54582
|
||||
oid sha256:001c89ba338b51d48f1e471e17306e84e74b0f952a856b65532ffeacbf52f2cb
|
||||
size 55982
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8afb610d9e86b324db22849457eb23419c4f3c240e5ee951a13ce8901003bca8
|
||||
size 55053
|
||||
oid sha256:bf299c3c52250b1a17411e3e1aef57a4293787c9a9777da6fda01bb6a0e387c1
|
||||
size 56466
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:75ff881add9f2968d19d8bb1d39239de5d99105027e91af209dfa5b45e644d2c
|
||||
size 35943
|
||||
oid sha256:ea03f6b0e0071910cf2b4279549fb0347cd6a9f8d0334830ce4f34f5824a1ef9
|
||||
size 37595
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1887d632d0994efec3dd7cecc41e3ad460c109baa1012d69e005cdc916e3cd24
|
||||
size 35925
|
||||
oid sha256:db4795c68fffb20f74d46664be9b7c4bfcaf941a8b3e27a7315f9525dd90d5c4
|
||||
size 37340
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8c0054dd1717833ae6f19726b58bffbf70bd4dc784b5f2774b194800a3adffed
|
||||
size 64973
|
||||
oid sha256:952df0869c400d8b1f5dc0a33cb7a8be1fa30d91929e57b7c33af0cfbc9cead4
|
||||
size 66043
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9be7321d8184c3d06cfeb54410c1f839892c4f836767e9b1950e70306cde6c54
|
||||
size 151090
|
||||
oid sha256:066067e34d47aa8d2631e64b2ea37d94bae77bdfb1af9337f34f724e1ff14cb6
|
||||
size 153589
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f07c25c053e9c4d5f7416cc00e101e13b7007b65c46ba13d0853d95ca43fddc4
|
||||
size 59950
|
||||
oid sha256:0d79861c18cbaf2f2ab738d0a5633f354c11a52b8a8e87c623e624ecdfcc6965
|
||||
size 60813
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6a9bc30b4d6ed100555ee553554848d20d93f25643e72eeef1826483ed307d9
|
||||
size 145940
|
||||
oid sha256:cde27ba5614f4bd5553d609b23e6dd78aea113c88443de42fa47aa85929c9455
|
||||
size 148235
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||

|
||||

|
||||
|
||||
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`:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:84e857521c20ea2458d9f9d724578e0495c06e75571c25104bf64a0be0be617d
|
||||
size 8028
|
||||
oid sha256:c32712afee568ce9dca8eeb6bd1dc0f072f9e9b9189ba0bbcd32f11317d73b98
|
||||
size 8160
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57211d5ac8021223ed288b9fd6f651ed901ec91599ed67d54ac540f3a2037937
|
||||
size 2205
|
||||
oid sha256:1933058e8eb0568b6a42be5bcb4aae28a478daa9b62ccb0e72d33623f9161cb1
|
||||
size 2246
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
159
crates/epaint/src/rounded_rect.rs
Normal file
159
crates/epaint/src/rounded_rect.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 _)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user