mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Merge remote-tracking branch 'origin/main' into lucas/fix-macos-fullscreen
This commit is contained in:
128
.typos.toml
128
.typos.toml
@@ -18,5 +18,133 @@ teselation = "tessellation"
|
||||
tessalation = "tessellation"
|
||||
tesselation = "tessellation"
|
||||
|
||||
|
||||
# Use the more common spelling
|
||||
adaptor = "adapter"
|
||||
adaptors = "adapters"
|
||||
|
||||
# For consistency we prefer American English:
|
||||
aeroplane = "airplane"
|
||||
analogue = "analog"
|
||||
analyse = "analyze"
|
||||
appetiser = "appetizer"
|
||||
arbour = "arbor"
|
||||
ardour = "arbor"
|
||||
armour = "armor"
|
||||
artefact = "artifact"
|
||||
authorise = "authorize"
|
||||
behaviour = "behavior"
|
||||
behavioural = "behavioral"
|
||||
British = "American"
|
||||
calibre = "caliber"
|
||||
# cancelled = "canceled" # winit uses this :(
|
||||
candour = "candor"
|
||||
capitalise = "capitalize"
|
||||
catalogue = "catalog"
|
||||
centre = "center"
|
||||
characterise = "characterize"
|
||||
chequerboard = "checkerboard"
|
||||
chequered = "checkered"
|
||||
civilise = "civilize"
|
||||
clamour = "clamor"
|
||||
colonise = "colonize"
|
||||
colour = "color"
|
||||
coloured = "colored"
|
||||
cosy = "cozy"
|
||||
criticise = "criticize"
|
||||
defence = "defense"
|
||||
demeanour = "demeanor"
|
||||
dialogue = "dialog"
|
||||
distil = "distill"
|
||||
doughnut = "donut"
|
||||
dramatise = "dramatize"
|
||||
draught = "draft"
|
||||
emphasise = "emphasize"
|
||||
endeavour = "endeavor"
|
||||
enrol = "enroll"
|
||||
epilogue = "epilog"
|
||||
equalise = "equalize"
|
||||
favour = "favor"
|
||||
favourite = "favorite"
|
||||
fibre = "fiber"
|
||||
flavour = "flavor"
|
||||
fulfil = "fufill"
|
||||
gaol = "jail"
|
||||
grey = "gray"
|
||||
greys = "grays"
|
||||
greyscale = "grayscale"
|
||||
harbour = "habor"
|
||||
honour = "honor"
|
||||
humour = "humor"
|
||||
instalment = "installment"
|
||||
instil = "instill"
|
||||
jewellery = "jewelry"
|
||||
kerb = "curb"
|
||||
labour = "labor"
|
||||
litre = "liter"
|
||||
lustre = "luster"
|
||||
meagre = "meager"
|
||||
metre = "meter"
|
||||
mobilise = "mobilize"
|
||||
monologue = "monolog"
|
||||
naturalise = "naturalize"
|
||||
neighbour = "neighbor"
|
||||
neighbourhood = "neighborhood"
|
||||
normalise = "normalize"
|
||||
normalised = "normalized"
|
||||
odour = "odor"
|
||||
offence = "offense"
|
||||
organise = "organize"
|
||||
parlour = "parlor"
|
||||
plough = "plow"
|
||||
popularise = "popularize"
|
||||
pretence = "pretense"
|
||||
programme = "program"
|
||||
prologue = "prolog"
|
||||
rancour = "rancor"
|
||||
realise = "realize"
|
||||
recognise = "recognize"
|
||||
recognised = "recognized"
|
||||
rigour = "rigor"
|
||||
rumour = "rumor"
|
||||
sabre = "saber"
|
||||
satirise = "satirize"
|
||||
saviour = "savior"
|
||||
savour = "savor"
|
||||
sceptical = "skeptical"
|
||||
sceptre = "scepter"
|
||||
sepulchre = "sepulcher"
|
||||
serialisation = "serialization"
|
||||
serialise = "serialize"
|
||||
serialised = "serialized"
|
||||
skilful = "skillful"
|
||||
sombre = "somber"
|
||||
specialisation = "specialization"
|
||||
specialise = "specialize"
|
||||
specialised = "specialized"
|
||||
splendour = "splendor"
|
||||
standardise = "standardize"
|
||||
sulphur = "sulfur"
|
||||
symbolise = "symbolize"
|
||||
theatre = "theater"
|
||||
tonne = "ton"
|
||||
travelogue = "travelog"
|
||||
tumour = "tumor"
|
||||
valour = "valor"
|
||||
vaporise = "vaporize"
|
||||
vigour = "vigor"
|
||||
|
||||
# null-terminated is the name of the wikipedia article!
|
||||
# https://en.wikipedia.org/wiki/Null-terminated_string
|
||||
nullterminated = "null-terminated"
|
||||
zeroterminated = "null-terminated"
|
||||
zero-terminated = "null-terminated"
|
||||
|
||||
|
||||
[files]
|
||||
extend-exclude = ["web_demo/egui_demo_app.js"] # auto-generated
|
||||
|
||||
[default]
|
||||
extend-ignore-re = [
|
||||
"#\\[doc\\(alias = .*", # We suggest "grey" in some doc
|
||||
]
|
||||
|
||||
@@ -36,7 +36,7 @@ There are snapshots test that might need to be updated.
|
||||
Run the tests with `UPDATE_SNAPSHOTS=true cargo test --workspace --all-features` to update all of them.
|
||||
If CI keeps complaining about snapshots (which could happen if you don't use macOS, snapshots in CI are currently
|
||||
rendered with macOS), you can instead run `./scripts/update_snapshots_from_ci.sh` to update your local snapshots from
|
||||
the last CI run of your PR (which will download the `test_results` artefact).
|
||||
the last CI run of your PR (which will download the `test_results` artifact).
|
||||
For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md).
|
||||
Snapshots and other big files are stored with git lfs. See [Working with git lfs](#working-with-git-lfs) for more info.
|
||||
If you see an `InvalidSignature` error when running snapshot tests, it's probably a problem related to git-lfs.
|
||||
|
||||
@@ -72,7 +72,7 @@ fn roaming_appdata() -> Option<PathBuf> {
|
||||
};
|
||||
|
||||
let path = if result == S_OK {
|
||||
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us.
|
||||
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a null-terminated string for us.
|
||||
let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) };
|
||||
Some(PathBuf::from(OsString::from_wide(path_slice)))
|
||||
} else {
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::web::string_from_js_value;
|
||||
use super::{
|
||||
AppRunner, Closure, DEBUG_RESIZE, JsCast as _, JsValue, WebRunner, button_from_mouse_event,
|
||||
location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, modifiers_from_wheel_event,
|
||||
native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme_dark, primary_touch_pos,
|
||||
push_touches, text_from_keyboard_event, theme_from_dark_mode, translate_key,
|
||||
native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme, primary_touch_pos,
|
||||
push_touches, text_from_keyboard_event, translate_key,
|
||||
};
|
||||
|
||||
use web_sys::{Document, EventTarget, ShadowRoot};
|
||||
@@ -469,16 +469,19 @@ fn install_color_scheme_change_event(
|
||||
runner_ref: &WebRunner,
|
||||
window: &web_sys::Window,
|
||||
) -> Result<(), JsValue> {
|
||||
if let Some(media_query_list) = prefers_color_scheme_dark(window)? {
|
||||
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
|
||||
&media_query_list,
|
||||
"change",
|
||||
|event, runner| {
|
||||
let theme = theme_from_dark_mode(event.matches());
|
||||
runner.input.raw.system_theme = Some(theme);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
for theme in [egui::Theme::Dark, egui::Theme::Light] {
|
||||
if let Some(media_query_list) = prefers_color_scheme(window, theme)? {
|
||||
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
|
||||
&media_query_list,
|
||||
"change",
|
||||
|_event, runner| {
|
||||
if let Some(theme) = super::system_theme() {
|
||||
runner.input.raw.system_theme = Some(theme);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -40,6 +40,7 @@ pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
|
||||
|
||||
pub use backend::*;
|
||||
|
||||
use egui::Theme;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{Document, MediaQueryList, Node};
|
||||
|
||||
@@ -113,24 +114,31 @@ pub fn native_pixels_per_point() -> f32 {
|
||||
///
|
||||
/// `None` means unknown.
|
||||
pub fn system_theme() -> Option<egui::Theme> {
|
||||
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
|
||||
.ok()??
|
||||
.matches();
|
||||
Some(theme_from_dark_mode(dark_mode))
|
||||
}
|
||||
|
||||
fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result<Option<MediaQueryList>, JsValue> {
|
||||
window.match_media("(prefers-color-scheme: dark)")
|
||||
}
|
||||
|
||||
fn theme_from_dark_mode(dark_mode: bool) -> egui::Theme {
|
||||
if dark_mode {
|
||||
egui::Theme::Dark
|
||||
let window = web_sys::window()?;
|
||||
if does_prefer_color_scheme(&window, Theme::Dark) == Some(true) {
|
||||
Some(Theme::Dark)
|
||||
} else if does_prefer_color_scheme(&window, Theme::Light) == Some(true) {
|
||||
Some(Theme::Light)
|
||||
} else {
|
||||
egui::Theme::Light
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn does_prefer_color_scheme(window: &web_sys::Window, theme: Theme) -> Option<bool> {
|
||||
Some(prefers_color_scheme(window, theme).ok()??.matches())
|
||||
}
|
||||
|
||||
fn prefers_color_scheme(
|
||||
window: &web_sys::Window,
|
||||
theme: Theme,
|
||||
) -> Result<Option<MediaQueryList>, JsValue> {
|
||||
let theme = match theme {
|
||||
Theme::Dark => "dark",
|
||||
Theme::Light => "light",
|
||||
};
|
||||
window.match_media(format!("(prefers-color-scheme: {theme})").as_str())
|
||||
}
|
||||
|
||||
/// Returns the canvas in client coordinates.
|
||||
fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
|
||||
let bounding_rect = canvas.get_bounding_client_rect();
|
||||
|
||||
@@ -279,13 +279,6 @@ impl WebPainter for WebPainterWgpu {
|
||||
Some((output_frame, capture_buffer))
|
||||
};
|
||||
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the commands: both the main buffer and user-defined ones.
|
||||
render_state
|
||||
.queue
|
||||
@@ -307,6 +300,16 @@ impl WebPainter for WebPainterWgpu {
|
||||
frame.present();
|
||||
}
|
||||
|
||||
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
|
||||
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
|
||||
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -97,9 +97,8 @@ fn vs_main(
|
||||
|
||||
@fragment
|
||||
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// We always have an sRGB aware texture at the moment.
|
||||
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
let tex_gamma = gamma_from_linear_rgba(tex_linear);
|
||||
// We expect "normal" textures that are NOT sRGB-aware.
|
||||
let tex_gamma = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
var out_color_gamma = in.color * tex_gamma;
|
||||
// Dither the float color down to eight bits to reduce banding.
|
||||
// This step is optional for egui backends.
|
||||
@@ -115,9 +114,8 @@ fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
|
||||
@fragment
|
||||
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// We always have an sRGB aware texture at the moment.
|
||||
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
let tex_gamma = gamma_from_linear_rgba(tex_linear);
|
||||
// We expect "normal" textures that are NOT sRGB-aware.
|
||||
let tex_gamma = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
var out_color_gamma = in.color * tex_gamma;
|
||||
// Dither the float color down to eight bits to reduce banding.
|
||||
// This step is optional for egui backends.
|
||||
|
||||
@@ -564,15 +564,6 @@ impl Renderer {
|
||||
);
|
||||
Cow::Borrowed(&image.pixels)
|
||||
}
|
||||
epaint::ImageData::Font(image) => {
|
||||
assert_eq!(
|
||||
width as usize * height as usize,
|
||||
image.pixels.len(),
|
||||
"Mismatch between texture size and texel count"
|
||||
);
|
||||
profiling::scope!("font -> sRGBA");
|
||||
Cow::Owned(image.srgba_pixels(None).collect::<Vec<epaint::Color32>>())
|
||||
}
|
||||
};
|
||||
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
|
||||
|
||||
@@ -638,9 +629,9 @@ impl Renderer {
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
||||
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
|
||||
})
|
||||
};
|
||||
let origin = wgpu::Origin3d::ZERO;
|
||||
@@ -699,7 +690,7 @@ impl Renderer {
|
||||
///
|
||||
/// This enables the application to reference the texture inside an image ui element.
|
||||
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
|
||||
/// the texture format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
|
||||
/// the texture format [`wgpu::TextureFormat::Rgba8Unorm`].
|
||||
pub fn register_native_texture(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
@@ -747,7 +738,7 @@ impl Renderer {
|
||||
/// This allows applications to specify individual minification/magnification filters as well as
|
||||
/// custom mipmap and tiling options.
|
||||
///
|
||||
/// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
|
||||
/// The texture must have the format [`wgpu::TextureFormat::Rgba8Unorm`].
|
||||
/// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored.
|
||||
#[expect(clippy::needless_pass_by_value)] // false positive
|
||||
pub fn register_native_texture_with_sampler_options(
|
||||
|
||||
@@ -81,7 +81,7 @@ impl<'a> Atom<'a> {
|
||||
wrap_mode = Some(TextWrapMode::Truncate);
|
||||
}
|
||||
|
||||
let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
|
||||
let (intrinsic, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
|
||||
|
||||
let size = self
|
||||
.size
|
||||
@@ -89,7 +89,7 @@ impl<'a> Atom<'a> {
|
||||
|
||||
SizedAtom {
|
||||
size,
|
||||
preferred_size: preferred,
|
||||
intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()),
|
||||
grow: self.grow,
|
||||
kind,
|
||||
}
|
||||
|
||||
@@ -81,11 +81,10 @@ impl<'a> AtomKind<'a> {
|
||||
) -> (Vec2, SizedAtomKind<'a>) {
|
||||
match self {
|
||||
AtomKind::Text(text) => {
|
||||
let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button);
|
||||
(
|
||||
galley.size(), // TODO(#5762): calculate the preferred size
|
||||
SizedAtomKind::Text(galley),
|
||||
)
|
||||
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
|
||||
let galley =
|
||||
text.into_galley(ui, Some(wrap_mode), available_size.x, TextStyle::Button);
|
||||
(galley.intrinsic_size, SizedAtomKind::Text(galley))
|
||||
}
|
||||
AtomKind::Image(image) => {
|
||||
let size = image.load_and_calc_size(ui, available_size);
|
||||
|
||||
@@ -183,10 +183,10 @@ impl<'a> AtomLayout<'a> {
|
||||
|
||||
let mut desired_width = 0.0;
|
||||
|
||||
// Preferred width / height is the ideal size of the widget, e.g. the size where the
|
||||
// intrinsic width / height is the ideal size of the widget, e.g. the size where the
|
||||
// text is not wrapped. Used to set Response::intrinsic_size.
|
||||
let mut preferred_width = 0.0;
|
||||
let mut preferred_height = 0.0;
|
||||
let mut intrinsic_width = 0.0;
|
||||
let mut intrinsic_height = 0.0;
|
||||
|
||||
let mut height: f32 = 0.0;
|
||||
|
||||
@@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> {
|
||||
if atoms.len() > 1 {
|
||||
let gap_space = gap * (atoms.len() as f32 - 1.0);
|
||||
desired_width += gap_space;
|
||||
preferred_width += gap_space;
|
||||
intrinsic_width += gap_space;
|
||||
}
|
||||
|
||||
for (idx, item) in atoms.into_iter().enumerate() {
|
||||
@@ -224,10 +224,10 @@ impl<'a> AtomLayout<'a> {
|
||||
let size = sized.size;
|
||||
|
||||
desired_width += size.x;
|
||||
preferred_width += sized.preferred_size.x;
|
||||
intrinsic_width += sized.intrinsic_size.x;
|
||||
|
||||
height = height.at_least(size.y);
|
||||
preferred_height = preferred_height.at_least(sized.preferred_size.y);
|
||||
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
|
||||
|
||||
sized_items.push(sized);
|
||||
}
|
||||
@@ -243,10 +243,10 @@ impl<'a> AtomLayout<'a> {
|
||||
let size = sized.size;
|
||||
|
||||
desired_width += size.x;
|
||||
preferred_width += sized.preferred_size.x;
|
||||
intrinsic_width += sized.intrinsic_size.x;
|
||||
|
||||
height = height.at_least(size.y);
|
||||
preferred_height = preferred_height.at_least(sized.preferred_size.y);
|
||||
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
|
||||
|
||||
sized_items.insert(index, sized);
|
||||
}
|
||||
@@ -259,7 +259,7 @@ impl<'a> AtomLayout<'a> {
|
||||
let mut response = ui.interact(rect, id, sense);
|
||||
|
||||
response.intrinsic_size =
|
||||
Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size));
|
||||
Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size));
|
||||
|
||||
AllocatedAtomLayout {
|
||||
sized_atoms: sized_items,
|
||||
|
||||
@@ -12,8 +12,8 @@ pub struct SizedAtom<'a> {
|
||||
/// size.x + gap.
|
||||
pub size: Vec2,
|
||||
|
||||
/// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`.
|
||||
pub preferred_size: Vec2,
|
||||
/// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`.
|
||||
pub intrinsic_size: Vec2,
|
||||
|
||||
pub kind: SizedAtomKind<'a>,
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ pub struct Area {
|
||||
new_pos: Option<Pos2>,
|
||||
fade_in: bool,
|
||||
layout: Layout,
|
||||
sizing_pass: bool,
|
||||
}
|
||||
|
||||
impl WidgetWithState for Area {
|
||||
@@ -147,6 +148,7 @@ impl Area {
|
||||
anchor: None,
|
||||
fade_in: true,
|
||||
layout: Layout::default(),
|
||||
sizing_pass: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +359,27 @@ impl Area {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
/// While true, a sizing pass will be done. This means the area will be invisible
|
||||
/// and the contents will be laid out to estimate the proper containing size of the area.
|
||||
/// If false, there will be no change to the default area behavior. This is useful if the
|
||||
/// area contents area dynamic and you need to need to make sure the area adjusts its size
|
||||
/// accordingly.
|
||||
///
|
||||
/// This should only be set to true during the specific frames you want force a sizing pass.
|
||||
/// Do NOT hard-code this as `.sizing_pass(true)`, as it will cause the area to never be
|
||||
/// visible.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - resize: If true, the area will be resized to fit its contents. False will keep the
|
||||
/// default area resizing behavior.
|
||||
///
|
||||
/// Default: `false`.
|
||||
#[inline]
|
||||
pub fn sizing_pass(mut self, resize: bool) -> Self {
|
||||
self.sizing_pass = resize;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Prepared {
|
||||
@@ -410,6 +433,7 @@ impl Area {
|
||||
constrain_rect,
|
||||
fade_in,
|
||||
layout,
|
||||
sizing_pass: force_sizing_pass,
|
||||
} = self;
|
||||
|
||||
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
|
||||
@@ -425,6 +449,10 @@ impl Area {
|
||||
interactable,
|
||||
last_became_visible_at: None,
|
||||
});
|
||||
if force_sizing_pass {
|
||||
sizing_pass = true;
|
||||
state.size = None;
|
||||
}
|
||||
state.pivot = pivot;
|
||||
state.interactable = interactable;
|
||||
if let Some(new_pos) = new_pos {
|
||||
|
||||
@@ -160,6 +160,7 @@ impl From<PopupKind> for UiKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "Call `.show()` to actually display the popup"]
|
||||
pub struct Popup<'a> {
|
||||
id: Id,
|
||||
ctx: Context,
|
||||
@@ -210,6 +211,57 @@ impl<'a> Popup<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a popup relative to some widget.
|
||||
/// The popup will be always open.
|
||||
///
|
||||
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
|
||||
pub fn from_response(response: &Response) -> Self {
|
||||
let mut popup = Self::new(
|
||||
response.id.with("popup"),
|
||||
response.ctx.clone(),
|
||||
response,
|
||||
response.layer_id,
|
||||
);
|
||||
popup.widget_clicked_elsewhere = response.clicked_elsewhere();
|
||||
popup
|
||||
}
|
||||
|
||||
/// Show a popup relative to some widget,
|
||||
/// toggling the open state based on the widget's click state.
|
||||
///
|
||||
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
|
||||
pub fn from_toggle_button_response(button_response: &Response) -> Self {
|
||||
Self::from_response(button_response)
|
||||
.open_memory(button_response.clicked().then_some(SetOpenCommand::Toggle))
|
||||
}
|
||||
|
||||
/// Show a popup when the widget was clicked.
|
||||
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
||||
pub fn menu(button_response: &Response) -> Self {
|
||||
Self::from_toggle_button_response(button_response)
|
||||
.kind(PopupKind::Menu)
|
||||
.layout(Layout::top_down_justified(Align::Min))
|
||||
.style(menu_style)
|
||||
.gap(0.0)
|
||||
}
|
||||
|
||||
/// Show a context menu when the widget was secondary clicked.
|
||||
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
||||
/// In contrast to [`Self::menu`], this will open at the pointer position.
|
||||
pub fn context_menu(response: &Response) -> Self {
|
||||
Self::menu(response)
|
||||
.open_memory(if response.secondary_clicked() {
|
||||
Some(SetOpenCommand::Bool(true))
|
||||
} else if response.clicked() {
|
||||
// Explicitly close the menu if the widget was clicked
|
||||
// Without this, the context menu would stay open if the user clicks the widget
|
||||
Some(SetOpenCommand::Bool(false))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.at_pointer_fixed()
|
||||
}
|
||||
|
||||
/// Set the kind of the popup. Used for [`Area::kind`] and [`Area::order`].
|
||||
#[inline]
|
||||
pub fn kind(mut self, kind: PopupKind) -> Self {
|
||||
@@ -242,49 +294,6 @@ impl<'a> Popup<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Show a popup relative to some widget.
|
||||
/// The popup will be always open.
|
||||
///
|
||||
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
|
||||
pub fn from_response(response: &Response) -> Self {
|
||||
let mut popup = Self::new(
|
||||
response.id.with("popup"),
|
||||
response.ctx.clone(),
|
||||
response,
|
||||
response.layer_id,
|
||||
);
|
||||
popup.widget_clicked_elsewhere = response.clicked_elsewhere();
|
||||
popup
|
||||
}
|
||||
|
||||
/// Show a popup when the widget was clicked.
|
||||
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
||||
pub fn menu(response: &Response) -> Self {
|
||||
Self::from_response(response)
|
||||
.open_memory(response.clicked().then_some(SetOpenCommand::Toggle))
|
||||
.kind(PopupKind::Menu)
|
||||
.layout(Layout::top_down_justified(Align::Min))
|
||||
.style(menu_style)
|
||||
.gap(0.0)
|
||||
}
|
||||
|
||||
/// Show a context menu when the widget was secondary clicked.
|
||||
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
||||
/// In contrast to [`Self::menu`], this will open at the pointer position.
|
||||
pub fn context_menu(response: &Response) -> Self {
|
||||
Self::menu(response)
|
||||
.open_memory(if response.secondary_clicked() {
|
||||
Some(SetOpenCommand::Bool(true))
|
||||
} else if response.clicked() {
|
||||
// Explicitly close the menu if the widget was clicked
|
||||
// Without this, the context menu would stay open if the user clicks the widget
|
||||
Some(SetOpenCommand::Bool(false))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.at_pointer_fixed()
|
||||
}
|
||||
|
||||
/// Force the popup to be open or closed.
|
||||
#[inline]
|
||||
pub fn open(mut self, open: bool) -> Self {
|
||||
@@ -484,37 +493,18 @@ impl<'a> Popup<'a> {
|
||||
self.gap,
|
||||
expected_popup_size,
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Show the popup.
|
||||
/// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
|
||||
/// no pointer.
|
||||
pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
|
||||
let best_align = self.get_best_align();
|
||||
let hover_pos = self.ctx.pointer_hover_pos();
|
||||
|
||||
let Popup {
|
||||
id,
|
||||
ctx,
|
||||
anchor,
|
||||
open_kind,
|
||||
close_behavior,
|
||||
kind,
|
||||
info,
|
||||
layer_id,
|
||||
rect_align: _,
|
||||
alternative_aligns: _,
|
||||
gap,
|
||||
widget_clicked_elsewhere,
|
||||
width,
|
||||
sense,
|
||||
layout,
|
||||
frame,
|
||||
style,
|
||||
} = self;
|
||||
|
||||
let hover_pos = ctx.pointer_hover_pos();
|
||||
if let OpenKind::Memory { set, .. } = open_kind {
|
||||
ctx.memory_mut(|mem| match set {
|
||||
let id = self.id;
|
||||
if let OpenKind::Memory { set } = self.open_kind {
|
||||
self.ctx.memory_mut(|mem| match set {
|
||||
Some(SetOpenCommand::Bool(open)) => {
|
||||
if open {
|
||||
match self.anchor {
|
||||
@@ -536,10 +526,32 @@ impl<'a> Popup<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
if !open_kind.is_open(id, &ctx) {
|
||||
if !self.open_kind.is_open(self.id, &self.ctx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let best_align = self.get_best_align();
|
||||
|
||||
let Popup {
|
||||
id,
|
||||
ctx,
|
||||
anchor,
|
||||
open_kind,
|
||||
close_behavior,
|
||||
kind,
|
||||
info,
|
||||
layer_id,
|
||||
rect_align: _,
|
||||
alternative_aligns: _,
|
||||
gap,
|
||||
widget_clicked_elsewhere,
|
||||
width,
|
||||
sense,
|
||||
layout,
|
||||
frame,
|
||||
style,
|
||||
} = self;
|
||||
|
||||
if kind != PopupKind::Tooltip {
|
||||
ctx.pass_state_mut(|fs| {
|
||||
fs.layers
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use emath::Align;
|
||||
use emath::{Align, NumExt as _};
|
||||
|
||||
use crate::{Layout, Ui, UiBuilder};
|
||||
|
||||
@@ -20,8 +20,13 @@ use crate::{Layout, Ui, UiBuilder};
|
||||
///
|
||||
/// If the parent is not wide enough to fit all widgets, the parent will be expanded to the right.
|
||||
///
|
||||
/// The left widgets are first added to the ui, left-to-right.
|
||||
/// Then the right widgets are added, right-to-left.
|
||||
/// The left widgets are added left-to-right.
|
||||
/// The right widgets are added right-to-left.
|
||||
///
|
||||
/// Which side is first depends on the configuration:
|
||||
/// - [`Sides::extend`] - left widgets are added first
|
||||
/// - [`Sides::shrink_left`] - right widgets are added first
|
||||
/// - [`Sides::shrink_right`] - left widgets are added first
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
@@ -40,6 +45,16 @@ use crate::{Layout, Ui, UiBuilder};
|
||||
pub struct Sides {
|
||||
height: Option<f32>,
|
||||
spacing: Option<f32>,
|
||||
kind: SidesKind,
|
||||
wrap_mode: Option<crate::TextWrapMode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
enum SidesKind {
|
||||
#[default]
|
||||
Extend,
|
||||
ShrinkLeft,
|
||||
ShrinkRight,
|
||||
}
|
||||
|
||||
impl Sides {
|
||||
@@ -68,58 +83,175 @@ impl Sides {
|
||||
self
|
||||
}
|
||||
|
||||
/// Try to shrink widgets on the left side.
|
||||
///
|
||||
/// Right widgets will be added first. The left [`Ui`]s max rect will be limited to the
|
||||
/// remaining space.
|
||||
#[inline]
|
||||
pub fn shrink_left(mut self) -> Self {
|
||||
self.kind = SidesKind::ShrinkLeft;
|
||||
self
|
||||
}
|
||||
|
||||
/// Try to shrink widgets on the right side.
|
||||
///
|
||||
/// Left widgets will be added first. The right [`Ui`]s max rect will be limited to the
|
||||
/// remaining space.
|
||||
#[inline]
|
||||
pub fn shrink_right(mut self) -> Self {
|
||||
self.kind = SidesKind::ShrinkRight;
|
||||
self
|
||||
}
|
||||
|
||||
/// Extend the left and right sides to fill the available space.
|
||||
///
|
||||
/// This is the default behavior.
|
||||
/// The left widgets will be added first, followed by the right widgets.
|
||||
#[inline]
|
||||
pub fn extend(mut self) -> Self {
|
||||
self.kind = SidesKind::Extend;
|
||||
self
|
||||
}
|
||||
|
||||
/// The text wrap mode for the shrinking side.
|
||||
///
|
||||
/// Does nothing if [`Self::extend`] is used (the default).
|
||||
#[inline]
|
||||
pub fn wrap_mode(mut self, wrap_mode: crate::TextWrapMode) -> Self {
|
||||
self.wrap_mode = Some(wrap_mode);
|
||||
self
|
||||
}
|
||||
|
||||
/// Truncate the text on the shrinking side.
|
||||
///
|
||||
/// This is a shortcut for [`Self::wrap_mode`].
|
||||
/// Does nothing if [`Self::extend`] is used (the default).
|
||||
#[inline]
|
||||
pub fn truncate(mut self) -> Self {
|
||||
self.wrap_mode = Some(crate::TextWrapMode::Truncate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Wrap the text on the shrinking side.
|
||||
///
|
||||
/// This is a shortcut for [`Self::wrap_mode`].
|
||||
/// Does nothing if [`Self::extend`] is used (the default).
|
||||
#[inline]
|
||||
pub fn wrap(mut self) -> Self {
|
||||
self.wrap_mode = Some(crate::TextWrapMode::Wrap);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn show<RetL, RetR>(
|
||||
self,
|
||||
ui: &mut Ui,
|
||||
add_left: impl FnOnce(&mut Ui) -> RetL,
|
||||
add_right: impl FnOnce(&mut Ui) -> RetR,
|
||||
) -> (RetL, RetR) {
|
||||
let Self { height, spacing } = self;
|
||||
let Self {
|
||||
height,
|
||||
spacing,
|
||||
mut kind,
|
||||
mut wrap_mode,
|
||||
} = self;
|
||||
let height = height.unwrap_or_else(|| ui.spacing().interact_size.y);
|
||||
let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing.x);
|
||||
|
||||
let mut top_rect = ui.available_rect_before_wrap();
|
||||
top_rect.max.y = top_rect.min.y + height;
|
||||
|
||||
let result_left;
|
||||
let result_right;
|
||||
|
||||
let left_rect = {
|
||||
let left_max_rect = top_rect;
|
||||
let mut left_ui = ui.new_child(
|
||||
UiBuilder::new()
|
||||
.max_rect(left_max_rect)
|
||||
.layout(Layout::left_to_right(Align::Center)),
|
||||
);
|
||||
result_left = add_left(&mut left_ui);
|
||||
left_ui.min_rect()
|
||||
};
|
||||
|
||||
let right_rect = {
|
||||
let right_max_rect = top_rect.with_min_x(left_rect.max.x);
|
||||
let mut right_ui = ui.new_child(
|
||||
UiBuilder::new()
|
||||
.max_rect(right_max_rect)
|
||||
.layout(Layout::right_to_left(Align::Center)),
|
||||
);
|
||||
result_right = add_right(&mut right_ui);
|
||||
right_ui.min_rect()
|
||||
};
|
||||
|
||||
let mut final_rect = left_rect.union(right_rect);
|
||||
let min_width = left_rect.width() + spacing + right_rect.width();
|
||||
|
||||
if ui.is_sizing_pass() {
|
||||
// Make as small as possible:
|
||||
final_rect.max.x = left_rect.min.x + min_width;
|
||||
} else {
|
||||
// If the rects overlap, make sure we expand the allocated rect so that the parent
|
||||
// ui knows we overflowed, and resizes:
|
||||
final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width);
|
||||
kind = SidesKind::Extend;
|
||||
wrap_mode = None;
|
||||
}
|
||||
|
||||
ui.advance_cursor_after_rect(final_rect);
|
||||
match kind {
|
||||
SidesKind::ShrinkLeft => {
|
||||
let (right_rect, result_right) = Self::create_ui(
|
||||
ui,
|
||||
top_rect,
|
||||
Layout::right_to_left(Align::Center),
|
||||
add_right,
|
||||
None,
|
||||
);
|
||||
let available_width = top_rect.width() - right_rect.width() - spacing;
|
||||
let left_rect_constraint =
|
||||
top_rect.with_max_x(top_rect.min.x + available_width.at_least(0.0));
|
||||
let (left_rect, result_left) = Self::create_ui(
|
||||
ui,
|
||||
left_rect_constraint,
|
||||
Layout::left_to_right(Align::Center),
|
||||
add_left,
|
||||
wrap_mode,
|
||||
);
|
||||
|
||||
(result_left, result_right)
|
||||
ui.advance_cursor_after_rect(left_rect.union(right_rect));
|
||||
(result_left, result_right)
|
||||
}
|
||||
SidesKind::ShrinkRight => {
|
||||
let (left_rect, result_left) = Self::create_ui(
|
||||
ui,
|
||||
top_rect,
|
||||
Layout::left_to_right(Align::Center),
|
||||
add_left,
|
||||
None,
|
||||
);
|
||||
let right_rect_constraint = top_rect.with_min_x(left_rect.max.x + spacing);
|
||||
let (right_rect, result_right) = Self::create_ui(
|
||||
ui,
|
||||
right_rect_constraint,
|
||||
Layout::right_to_left(Align::Center),
|
||||
add_right,
|
||||
wrap_mode,
|
||||
);
|
||||
|
||||
ui.advance_cursor_after_rect(left_rect.union(right_rect));
|
||||
(result_left, result_right)
|
||||
}
|
||||
SidesKind::Extend => {
|
||||
let (left_rect, result_left) = Self::create_ui(
|
||||
ui,
|
||||
top_rect,
|
||||
Layout::left_to_right(Align::Center),
|
||||
add_left,
|
||||
None,
|
||||
);
|
||||
let right_max_rect = top_rect.with_min_x(left_rect.max.x);
|
||||
let (right_rect, result_right) = Self::create_ui(
|
||||
ui,
|
||||
right_max_rect,
|
||||
Layout::right_to_left(Align::Center),
|
||||
add_right,
|
||||
None,
|
||||
);
|
||||
|
||||
let mut final_rect = left_rect.union(right_rect);
|
||||
let min_width = left_rect.width() + spacing + right_rect.width();
|
||||
|
||||
if ui.is_sizing_pass() {
|
||||
final_rect.max.x = left_rect.min.x + min_width;
|
||||
} else {
|
||||
final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width);
|
||||
}
|
||||
|
||||
ui.advance_cursor_after_rect(final_rect);
|
||||
(result_left, result_right)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_ui<Ret>(
|
||||
ui: &mut Ui,
|
||||
max_rect: emath::Rect,
|
||||
layout: Layout,
|
||||
add_content: impl FnOnce(&mut Ui) -> Ret,
|
||||
wrap_mode: Option<crate::TextWrapMode>,
|
||||
) -> (emath::Rect, Ret) {
|
||||
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(max_rect).layout(layout));
|
||||
if let Some(wrap_mode) = wrap_mode {
|
||||
child_ui.style_mut().wrap_mode = Some(wrap_mode);
|
||||
}
|
||||
let result = add_content(&mut child_ui);
|
||||
(child_ui.min_rect(), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ impl Default for WrappedTextureManager {
|
||||
// Will be filled in later
|
||||
let font_id = tex_mngr.alloc(
|
||||
"egui_font_texture".into(),
|
||||
epaint::FontImage::new([0, 0]).into(),
|
||||
epaint::ColorImage::filled([0, 0], Color32::TRANSPARENT).into(),
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -610,6 +610,8 @@ impl ContextImpl {
|
||||
log::trace!("Adding new fonts");
|
||||
}
|
||||
|
||||
let text_alpha_from_coverage = self.memory.options.style().visuals.text_alpha_from_coverage;
|
||||
|
||||
let mut is_new = false;
|
||||
|
||||
let fonts = self
|
||||
@@ -624,13 +626,14 @@ impl ContextImpl {
|
||||
Fonts::new(
|
||||
pixels_per_point,
|
||||
max_texture_side,
|
||||
text_alpha_from_coverage,
|
||||
self.font_definitions.clone(),
|
||||
)
|
||||
});
|
||||
|
||||
{
|
||||
profiling::scope!("Fonts::begin_pass");
|
||||
fonts.begin_pass(pixels_per_point, max_texture_side);
|
||||
fonts.begin_pass(pixels_per_point, max_texture_side, text_alpha_from_coverage);
|
||||
}
|
||||
|
||||
if is_new && self.memory.options.preload_font_glyphs {
|
||||
@@ -1149,7 +1152,7 @@ impl Context {
|
||||
ID clashes happens when things like Windows or CollapsingHeaders share names,\n\
|
||||
or when things like Plot and Grid:s aren't given unique id_salt:s.\n\n\
|
||||
Sometimes the solution is to use ui.push_id.",
|
||||
if below { "above" } else { "below" })
|
||||
if below { "above" } else { "below" }),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1216,6 +1219,51 @@ impl Context {
|
||||
self.accesskit_node_builder(w.id, |builder| res.fill_accesskit_node_common(builder));
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.write(|ctx| {
|
||||
use crate::{Align, pass_state::ScrollTarget, style::ScrollAnimation};
|
||||
let viewport = ctx.viewport_for(ctx.viewport_id());
|
||||
|
||||
viewport
|
||||
.input
|
||||
.consume_accesskit_action_requests(res.id, |request| {
|
||||
// TODO(lucasmerlin): Correctly handle the scroll unit:
|
||||
// https://github.com/AccessKit/accesskit/blob/e639c0e0d8ccbfd9dff302d972fa06f9766d608e/common/src/lib.rs#L2621
|
||||
const DISTANCE: f32 = 100.0;
|
||||
|
||||
match &request.action {
|
||||
accesskit::Action::ScrollIntoView => {
|
||||
viewport.this_pass.scroll_target = [
|
||||
Some(ScrollTarget::new(
|
||||
res.rect.x_range(),
|
||||
Some(Align::Center),
|
||||
ScrollAnimation::none(),
|
||||
)),
|
||||
Some(ScrollTarget::new(
|
||||
res.rect.y_range(),
|
||||
Some(Align::Center),
|
||||
ScrollAnimation::none(),
|
||||
)),
|
||||
];
|
||||
}
|
||||
accesskit::Action::ScrollDown => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::UP;
|
||||
}
|
||||
accesskit::Action::ScrollUp => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::DOWN;
|
||||
}
|
||||
accesskit::Action::ScrollLeft => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::LEFT;
|
||||
}
|
||||
accesskit::Action::ScrollRight => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::RIGHT;
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
true
|
||||
});
|
||||
});
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ pub fn hit_test(
|
||||
}
|
||||
}
|
||||
|
||||
close.retain(|rect| !rect.interact_rect.any_nan()); // Protect against bad input and transforms
|
||||
|
||||
// When using layer transforms it is common to stack layers close to each other.
|
||||
// For instance, you may have a resize-separator on a panel, with two
|
||||
// transform-layers on either side.
|
||||
|
||||
@@ -824,6 +824,23 @@ impl InputState {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn consume_accesskit_action_requests(
|
||||
&mut self,
|
||||
id: crate::Id,
|
||||
mut consume: impl FnMut(&accesskit::ActionRequest) -> bool,
|
||||
) {
|
||||
let accesskit_id = id.accesskit_id();
|
||||
self.events.retain(|event| {
|
||||
if let Event::AccessKitActionRequest(request) = event {
|
||||
if request.target == accesskit_id {
|
||||
return !consume(request);
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
|
||||
self.accesskit_action_requests(id, action).next().is_some()
|
||||
@@ -1448,7 +1465,10 @@ impl PointerState {
|
||||
}
|
||||
|
||||
if let Some(pos) = self.hover_pos() {
|
||||
return rect.intersects_ray(pos, self.direction());
|
||||
let dir = self.direction();
|
||||
if dir != Vec2::ZERO {
|
||||
return rect.intersects_ray(pos, self.direction());
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -161,12 +161,10 @@
|
||||
//!
|
||||
//! * egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`.
|
||||
//! * Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`).
|
||||
//! * egui prefers linear color spaces for all blending so:
|
||||
//! * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`).
|
||||
//! * Otherwise: remember to decode gamma in the fragment shader.
|
||||
//! * Decode the gamma of the incoming vertex colors in your vertex shader.
|
||||
//! * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`).
|
||||
//! * Otherwise: gamma-encode the colors before you write them again.
|
||||
//! * egui prefers gamma color spaces for all blending so:
|
||||
//! * Do NOT use an sRGBA-aware texture (NOT `GL_SRGB8_ALPHA8`).
|
||||
//! * Multiply texture and vertex colors in gamma space
|
||||
//! * Turn OFF sRGBA/gamma framebuffer (NO `GL_FRAMEBUFFER_SRGB`).
|
||||
//!
|
||||
//!
|
||||
//! # Understanding immediate mode
|
||||
@@ -467,7 +465,7 @@ pub use emath::{
|
||||
remap_clamp, vec2,
|
||||
};
|
||||
pub use epaint::{
|
||||
ClippedPrimitive, ColorImage, CornerRadius, FontImage, ImageData, Margin, Mesh, PaintCallback,
|
||||
ClippedPrimitive, ColorImage, CornerRadius, ImageData, Margin, Mesh, PaintCallback,
|
||||
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex,
|
||||
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
|
||||
textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta},
|
||||
|
||||
@@ -408,11 +408,11 @@ impl Options {
|
||||
.show(ui, |ui| {
|
||||
theme_preference.radio_buttons(ui);
|
||||
|
||||
std::sync::Arc::make_mut(match theme {
|
||||
let style = std::sync::Arc::make_mut(match theme {
|
||||
Theme::Dark => dark_style,
|
||||
Theme::Light => light_style,
|
||||
})
|
||||
.ui(ui);
|
||||
});
|
||||
style.ui(ui);
|
||||
});
|
||||
|
||||
CollapsingHeader::new("✒ Painting")
|
||||
|
||||
@@ -89,9 +89,32 @@ impl ThemePreference {
|
||||
/// Show radio-buttons to switch between light mode, dark mode and following the system theme.
|
||||
pub fn radio_buttons(&mut self, ui: &mut crate::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(self, Self::Light, "☀ Light");
|
||||
ui.selectable_value(self, Self::Dark, "🌙 Dark");
|
||||
ui.selectable_value(self, Self::System, "💻 System");
|
||||
let system_theme = ui.ctx().input(|i| i.raw.system_theme);
|
||||
|
||||
ui.selectable_value(self, Self::System, "💻 System")
|
||||
.on_hover_ui(|ui| {
|
||||
ui.label("Follow the system theme preference.");
|
||||
|
||||
ui.add_space(4.0);
|
||||
|
||||
if let Some(system_theme) = system_theme {
|
||||
ui.label(format!(
|
||||
"The current system theme is: {}",
|
||||
match system_theme {
|
||||
Theme::Dark => "dark",
|
||||
Theme::Light => "light",
|
||||
}
|
||||
));
|
||||
} else {
|
||||
ui.label("The system theme is unknown.");
|
||||
}
|
||||
});
|
||||
|
||||
ui.selectable_value(self, Self::Dark, "🌙 Dark")
|
||||
.on_hover_text("Use the dark mode theme");
|
||||
|
||||
ui.selectable_value(self, Self::Light, "☀ Light")
|
||||
.on_hover_text("Use the light mode theme");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ impl Painter {
|
||||
}
|
||||
|
||||
/// If set, colors will be modified to look like this
|
||||
#[deprecated = "Use `multiply_opacity` instead"]
|
||||
pub fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
|
||||
self.fade_to_color = fade_to_color;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#![allow(clippy::if_same_then_else)]
|
||||
|
||||
use emath::Align;
|
||||
use epaint::{CornerRadius, Shadow, Stroke, text::FontTweak};
|
||||
use epaint::{AlphaFromCoverage, CornerRadius, Shadow, Stroke, text::FontTweak};
|
||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
@@ -11,6 +11,7 @@ use crate::{
|
||||
WidgetText,
|
||||
ecolor::Color32,
|
||||
emath::{Rangef, Rect, Vec2, pos2, vec2},
|
||||
reset_button_with,
|
||||
};
|
||||
|
||||
/// How to format numbers in e.g. a [`crate::DragValue`].
|
||||
@@ -920,6 +921,9 @@ pub struct Visuals {
|
||||
/// this is more to provide a convenient summary of the rest of the settings.
|
||||
pub dark_mode: bool,
|
||||
|
||||
/// ADVANCED: Controls how we render text.
|
||||
pub text_alpha_from_coverage: AlphaFromCoverage,
|
||||
|
||||
/// Override default text color for all text.
|
||||
///
|
||||
/// This is great for setting the color of text for any widget.
|
||||
@@ -935,6 +939,17 @@ pub struct Visuals {
|
||||
/// it is disabled, non-interactive, hovered etc.
|
||||
pub override_text_color: Option<Color32>,
|
||||
|
||||
/// How strong "weak" text is.
|
||||
///
|
||||
/// Ignored if [`Self::weak_text_color`] is set.
|
||||
pub weak_text_alpha: f32,
|
||||
|
||||
/// Color of "weak" text.
|
||||
///
|
||||
/// If `None`, the color is [`Self::text_color`]
|
||||
/// multiplied by [`Self::weak_text_alpha`].
|
||||
pub weak_text_color: Option<Color32>,
|
||||
|
||||
/// Visual styles of widgets
|
||||
pub widgets: Widgets,
|
||||
|
||||
@@ -952,6 +967,11 @@ pub struct Visuals {
|
||||
/// that needs to look different from other interactive stuff.
|
||||
pub extreme_bg_color: Color32,
|
||||
|
||||
/// The background color of [`crate::TextEdit`].
|
||||
///
|
||||
/// Defaults to [`Self::extreme_bg_color`].
|
||||
pub text_edit_bg_color: Option<Color32>,
|
||||
|
||||
/// Background color behind code-styled monospaced labels.
|
||||
pub code_bg_color: Color32,
|
||||
|
||||
@@ -1019,6 +1039,9 @@ pub struct Visuals {
|
||||
|
||||
/// How to display numeric color values.
|
||||
pub numeric_color_space: NumericColorSpace,
|
||||
|
||||
/// How much to modify the alpha of a disabled widget.
|
||||
pub disabled_alpha: f32,
|
||||
}
|
||||
|
||||
impl Visuals {
|
||||
@@ -1034,7 +1057,8 @@ impl Visuals {
|
||||
}
|
||||
|
||||
pub fn weak_text_color(&self) -> Color32 {
|
||||
self.gray_out(self.text_color())
|
||||
self.weak_text_color
|
||||
.unwrap_or_else(|| self.text_color().gamma_multiply(self.weak_text_alpha))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
@@ -1042,6 +1066,11 @@ impl Visuals {
|
||||
self.widgets.active.text_color()
|
||||
}
|
||||
|
||||
/// The background color of [`crate::TextEdit`].
|
||||
pub fn text_edit_bg_color(&self) -> Color32 {
|
||||
self.text_edit_bg_color.unwrap_or(self.extreme_bg_color)
|
||||
}
|
||||
|
||||
/// Window background color.
|
||||
#[inline(always)]
|
||||
pub fn window_fill(&self) -> Color32 {
|
||||
@@ -1054,17 +1083,32 @@ impl Visuals {
|
||||
}
|
||||
|
||||
/// When fading out things, we fade the colors towards this.
|
||||
// TODO(emilk): replace with an alpha
|
||||
#[inline(always)]
|
||||
#[deprecated = "Use disabled_alpha(). Fading is now handled by modifying the alpha channel."]
|
||||
pub fn fade_out_to_color(&self) -> Color32 {
|
||||
self.widgets.noninteractive.weak_bg_fill
|
||||
}
|
||||
|
||||
/// Returned a "grayed out" version of the given color.
|
||||
/// Disabled widgets have their alpha modified by this.
|
||||
#[inline(always)]
|
||||
pub fn disabled_alpha(&self) -> f32 {
|
||||
self.disabled_alpha
|
||||
}
|
||||
|
||||
/// Returns a "disabled" version of the given color.
|
||||
///
|
||||
/// This function modifies the opcacity of the given color.
|
||||
/// If this is undesirable use [`gray_out`](Self::gray_out).
|
||||
#[inline(always)]
|
||||
pub fn disable(&self, color: Color32) -> Color32 {
|
||||
color.gamma_multiply(self.disabled_alpha())
|
||||
}
|
||||
|
||||
/// Returns a "grayed out" version of the given color.
|
||||
#[doc(alias = "grey_out")]
|
||||
#[inline(always)]
|
||||
pub fn gray_out(&self, color: Color32) -> Color32 {
|
||||
crate::ecolor::tint_color_towards(color, self.fade_out_to_color())
|
||||
crate::ecolor::tint_color_towards(color, self.widgets.noninteractive.weak_bg_fill)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1333,12 +1377,16 @@ impl Visuals {
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
dark_mode: true,
|
||||
text_alpha_from_coverage: AlphaFromCoverage::DARK_MODE_DEFAULT,
|
||||
override_text_color: None,
|
||||
weak_text_alpha: 0.6,
|
||||
weak_text_color: None,
|
||||
widgets: Widgets::default(),
|
||||
selection: Selection::default(),
|
||||
hyperlink_color: Color32::from_rgb(90, 170, 255),
|
||||
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
|
||||
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
|
||||
text_edit_bg_color: None, // use `extreme_bg_color` by default
|
||||
code_bg_color: Color32::from_gray(64),
|
||||
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
|
||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||
@@ -1384,6 +1432,7 @@ impl Visuals {
|
||||
image_loading_spinners: true,
|
||||
|
||||
numeric_color_space: NumericColorSpace::GammaByte,
|
||||
disabled_alpha: 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1391,6 +1440,7 @@ impl Visuals {
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
dark_mode: false,
|
||||
text_alpha_from_coverage: AlphaFromCoverage::LIGHT_MODE_DEFAULT,
|
||||
widgets: Widgets::light(),
|
||||
selection: Selection::light(),
|
||||
hyperlink_color: Color32::from_rgb(0, 155, 255),
|
||||
@@ -1680,11 +1730,11 @@ impl Style {
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
ui.collapsing("🔠 Text Styles", |ui| text_styles_ui(ui, text_styles));
|
||||
ui.collapsing("🔠 Text styles", |ui| text_styles_ui(ui, text_styles));
|
||||
ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
|
||||
ui.collapsing("☝ Interaction", |ui| interaction.ui(ui));
|
||||
ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
|
||||
ui.collapsing("🔄 Scroll Animation", |ui| scroll_animation.ui(ui));
|
||||
ui.collapsing("🔄 Scroll animation", |ui| scroll_animation.ui(ui));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
ui.collapsing("🐛 Debug", |ui| debug.ui(ui));
|
||||
@@ -2022,13 +2072,17 @@ impl WidgetVisuals {
|
||||
impl Visuals {
|
||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
dark_mode: _,
|
||||
dark_mode,
|
||||
text_alpha_from_coverage,
|
||||
override_text_color: _,
|
||||
weak_text_alpha,
|
||||
weak_text_color,
|
||||
widgets,
|
||||
selection,
|
||||
hyperlink_color,
|
||||
faint_bg_color,
|
||||
extreme_bg_color,
|
||||
text_edit_bg_color,
|
||||
code_bg_color,
|
||||
warn_fg_color,
|
||||
error_fg_color,
|
||||
@@ -2063,44 +2117,115 @@ impl Visuals {
|
||||
image_loading_spinners,
|
||||
|
||||
numeric_color_space,
|
||||
disabled_alpha,
|
||||
} = self;
|
||||
|
||||
ui.collapsing("Background Colors", |ui| {
|
||||
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
|
||||
ui_color(ui, window_fill, "Windows");
|
||||
ui_color(ui, panel_fill, "Panels");
|
||||
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
|
||||
"Used for faint accentuation of interactive things, like striped grids.",
|
||||
);
|
||||
ui_color(ui, extreme_bg_color, "Extreme")
|
||||
.on_hover_text("Background of plots and paintings");
|
||||
fn ui_optional_color(
|
||||
ui: &mut Ui,
|
||||
color: &mut Option<Color32>,
|
||||
default_value: Color32,
|
||||
label: impl Into<WidgetText>,
|
||||
) -> Response {
|
||||
let label_response = ui.label(label);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let mut set = color.is_some();
|
||||
ui.checkbox(&mut set, "");
|
||||
if set {
|
||||
let color = color.get_or_insert(default_value);
|
||||
ui.color_edit_button_srgba(color);
|
||||
} else {
|
||||
*color = None;
|
||||
};
|
||||
});
|
||||
|
||||
ui.end_row();
|
||||
|
||||
label_response
|
||||
}
|
||||
|
||||
ui.collapsing("Background colors", |ui| {
|
||||
Grid::new("background_colors")
|
||||
.num_columns(2)
|
||||
.show(ui, |ui| {
|
||||
fn ui_color(
|
||||
ui: &mut Ui,
|
||||
color: &mut Color32,
|
||||
label: impl Into<WidgetText>,
|
||||
) -> Response {
|
||||
let label_response = ui.label(label);
|
||||
ui.color_edit_button_srgba(color);
|
||||
ui.end_row();
|
||||
label_response
|
||||
}
|
||||
|
||||
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
|
||||
ui_color(ui, window_fill, "Windows");
|
||||
ui_color(ui, panel_fill, "Panels");
|
||||
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
|
||||
"Used for faint accentuation of interactive things, like striped grids.",
|
||||
);
|
||||
ui_color(ui, extreme_bg_color, "Extreme")
|
||||
.on_hover_text("Background of plots and paintings");
|
||||
|
||||
ui_optional_color(ui, text_edit_bg_color, *extreme_bg_color, "TextEdit")
|
||||
.on_hover_text("Background of TextEdit");
|
||||
});
|
||||
});
|
||||
|
||||
ui.collapsing("Text color", |ui| {
|
||||
ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label");
|
||||
ui_text_color(
|
||||
ui,
|
||||
&mut widgets.inactive.fg_stroke.color,
|
||||
"Unhovered button",
|
||||
);
|
||||
ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button");
|
||||
ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button");
|
||||
fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into<RichText>) {
|
||||
ui.label(label.into().color(*color));
|
||||
ui.color_edit_button_srgba(color);
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
ui_text_color(ui, warn_fg_color, RichText::new("Warnings"));
|
||||
ui_text_color(ui, error_fg_color, RichText::new("Errors"));
|
||||
Grid::new("text_color").num_columns(2).show(ui, |ui| {
|
||||
ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label");
|
||||
|
||||
ui_text_color(ui, hyperlink_color, "hyperlink_color");
|
||||
ui_text_color(
|
||||
ui,
|
||||
&mut widgets.inactive.fg_stroke.color,
|
||||
"Unhovered button",
|
||||
);
|
||||
ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button");
|
||||
ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button");
|
||||
|
||||
ui_color(ui, code_bg_color, RichText::new("Code background").code()).on_hover_ui(
|
||||
|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("For monospaced inlined text ");
|
||||
ui.code("like this");
|
||||
ui.label(".");
|
||||
ui_text_color(ui, warn_fg_color, RichText::new("Warnings"));
|
||||
ui_text_color(ui, error_fg_color, RichText::new("Errors"));
|
||||
|
||||
ui_text_color(ui, hyperlink_color, "hyperlink_color");
|
||||
|
||||
ui.label(RichText::new("Code background").code())
|
||||
.on_hover_ui(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("For monospaced inlined text ");
|
||||
ui.code("like this");
|
||||
ui.label(".");
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
ui.color_edit_button_srgba(code_bg_color);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Weak text alpha");
|
||||
ui.add_enabled(
|
||||
weak_text_color.is_none(),
|
||||
DragValue::new(weak_text_alpha).speed(0.01).range(0.0..=1.0),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui_optional_color(
|
||||
ui,
|
||||
weak_text_color,
|
||||
widgets.noninteractive.text_color(),
|
||||
"Weak text color",
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(4.0);
|
||||
|
||||
text_alpha_from_coverage_ui(ui, text_alpha_from_coverage);
|
||||
});
|
||||
|
||||
ui.collapsing("Text cursor", |ui| {
|
||||
@@ -2191,12 +2316,60 @@ impl Visuals {
|
||||
ui.label("Color picker type");
|
||||
numeric_color_space.toggle_button_ui(ui);
|
||||
});
|
||||
|
||||
ui.add(Slider::new(disabled_alpha, 0.0..=1.0).text("Disabled element alpha"));
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| reset_button(ui, self, "Reset visuals"));
|
||||
let dark_mode = *dark_mode;
|
||||
ui.vertical_centered(|ui| {
|
||||
reset_button_with(
|
||||
ui,
|
||||
self,
|
||||
"Reset visuals",
|
||||
if dark_mode {
|
||||
Self::dark()
|
||||
} else {
|
||||
Self::light()
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn text_alpha_from_coverage_ui(ui: &mut Ui, text_alpha_from_coverage: &mut AlphaFromCoverage) {
|
||||
let mut dark_mode_special =
|
||||
*text_alpha_from_coverage == AlphaFromCoverage::TwoCoverageMinusCoverageSq;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Text rendering:");
|
||||
|
||||
ui.checkbox(&mut dark_mode_special, "Dark-mode special");
|
||||
|
||||
if dark_mode_special {
|
||||
*text_alpha_from_coverage = AlphaFromCoverage::TwoCoverageMinusCoverageSq;
|
||||
} else {
|
||||
let mut gamma = match text_alpha_from_coverage {
|
||||
AlphaFromCoverage::Linear => 1.0,
|
||||
AlphaFromCoverage::Gamma(gamma) => *gamma,
|
||||
AlphaFromCoverage::TwoCoverageMinusCoverageSq => 0.5, // approximately the same
|
||||
};
|
||||
|
||||
ui.add(
|
||||
DragValue::new(&mut gamma)
|
||||
.speed(0.01)
|
||||
.range(0.1..=4.0)
|
||||
.prefix("Gamma: "),
|
||||
);
|
||||
|
||||
if gamma == 1.0 {
|
||||
*text_alpha_from_coverage = AlphaFromCoverage::Linear;
|
||||
} else {
|
||||
*text_alpha_from_coverage = AlphaFromCoverage::Gamma(gamma);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl TextCursorStyle {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
let Self {
|
||||
@@ -2310,22 +2483,6 @@ fn two_drag_values(value: &mut Vec2, range: std::ops::RangeInclusive<f32>) -> im
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_color(ui: &mut Ui, color: &mut Color32, label: impl Into<WidgetText>) -> Response {
|
||||
ui.horizontal(|ui| {
|
||||
ui.color_edit_button_srgba(color);
|
||||
ui.label(label);
|
||||
})
|
||||
.response
|
||||
}
|
||||
|
||||
fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into<RichText>) -> Response {
|
||||
ui.horizontal(|ui| {
|
||||
ui.color_edit_button_srgba(color);
|
||||
ui.label(label.into().color(*color));
|
||||
})
|
||||
.response
|
||||
}
|
||||
|
||||
impl HandleShape {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::{
|
||||
vec2, widgets,
|
||||
widgets::{
|
||||
Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link, RadioButton,
|
||||
SelectableLabel, Separator, Spinner, TextEdit, Widget, color_picker,
|
||||
Separator, Spinner, TextEdit, Widget, color_picker,
|
||||
},
|
||||
};
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -522,7 +522,7 @@ impl Ui {
|
||||
self.enabled = false;
|
||||
if self.is_visible() {
|
||||
self.painter
|
||||
.set_fade_to_color(Some(self.visuals().fade_out_to_color()));
|
||||
.multiply_opacity(self.visuals().disabled_alpha());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2077,13 +2077,13 @@ impl Ui {
|
||||
Checkbox::new(checked, atoms).ui(self)
|
||||
}
|
||||
|
||||
/// Acts like a checkbox, but looks like a [`SelectableLabel`].
|
||||
/// Acts like a checkbox, but looks like a [`Button::selectable`].
|
||||
///
|
||||
/// Click to toggle to bool.
|
||||
///
|
||||
/// See also [`Self::checkbox`].
|
||||
pub fn toggle_value(&mut self, selected: &mut bool, text: impl Into<WidgetText>) -> Response {
|
||||
let mut response = self.selectable_label(*selected, text);
|
||||
pub fn toggle_value<'a>(&mut self, selected: &mut bool, atoms: impl IntoAtoms<'a>) -> Response {
|
||||
let mut response = self.selectable_label(*selected, atoms);
|
||||
if response.clicked() {
|
||||
*selected = !*selected;
|
||||
response.mark_changed();
|
||||
@@ -2134,10 +2134,10 @@ impl Ui {
|
||||
|
||||
/// Show a label which can be selected or not.
|
||||
///
|
||||
/// See also [`SelectableLabel`] and [`Self::toggle_value`].
|
||||
/// See also [`Button::selectable`] and [`Self::toggle_value`].
|
||||
#[must_use = "You should check if the user clicked this with `if ui.selectable_label(…).clicked() { … } "]
|
||||
pub fn selectable_label(&mut self, checked: bool, text: impl Into<WidgetText>) -> Response {
|
||||
SelectableLabel::new(checked, text).ui(self)
|
||||
pub fn selectable_label<'a>(&mut self, checked: bool, text: impl IntoAtoms<'a>) -> Response {
|
||||
Button::selectable(checked, text).ui(self)
|
||||
}
|
||||
|
||||
/// Show selectable text. It is selected if `*current_value == selected_value`.
|
||||
@@ -2145,12 +2145,12 @@ impl Ui {
|
||||
///
|
||||
/// Example: `ui.selectable_value(&mut my_enum, Enum::Alternative, "Alternative")`.
|
||||
///
|
||||
/// See also [`SelectableLabel`] and [`Self::toggle_value`].
|
||||
pub fn selectable_value<Value: PartialEq>(
|
||||
/// See also [`Button::selectable`] and [`Self::toggle_value`].
|
||||
pub fn selectable_value<'a, Value: PartialEq>(
|
||||
&mut self,
|
||||
current_value: &mut Value,
|
||||
selected_value: Value,
|
||||
text: impl Into<WidgetText>,
|
||||
text: impl IntoAtoms<'a>,
|
||||
) -> Response {
|
||||
let mut response = self.selectable_label(*current_value == selected_value, text);
|
||||
if response.clicked() && *current_value != selected_value {
|
||||
@@ -2963,8 +2963,8 @@ impl Ui {
|
||||
|
||||
if is_anything_being_dragged && !can_accept_what_is_being_dragged {
|
||||
// When dragging something else, show that it can't be dropped here:
|
||||
fill = self.visuals().gray_out(fill);
|
||||
stroke.color = self.visuals().gray_out(stroke.color);
|
||||
fill = self.visuals().disable(fill);
|
||||
stroke.color = self.visuals().disable(stroke.color);
|
||||
}
|
||||
|
||||
frame.frame.fill = fill;
|
||||
|
||||
@@ -438,7 +438,7 @@ impl ViewportBuilder {
|
||||
}
|
||||
|
||||
/// macOS: Set to `true` to allow the window to be moved by dragging the background.
|
||||
/// Enabling this feature can result in unexpected behaviour with draggable UI widgets such as sliders.
|
||||
/// Enabling this feature can result in unexpected behavior with draggable UI widgets such as sliders.
|
||||
#[inline]
|
||||
pub fn with_movable_by_background(mut self, value: bool) -> Self {
|
||||
self.movable_by_window_background = Some(value);
|
||||
|
||||
@@ -29,6 +29,7 @@ pub struct Button<'a> {
|
||||
stroke: Option<Stroke>,
|
||||
small: bool,
|
||||
frame: Option<bool>,
|
||||
frame_when_inactive: bool,
|
||||
min_size: Vec2,
|
||||
corner_radius: Option<CornerRadius>,
|
||||
selected: bool,
|
||||
@@ -44,6 +45,7 @@ impl<'a> Button<'a> {
|
||||
stroke: None,
|
||||
small: false,
|
||||
frame: None,
|
||||
frame_when_inactive: true,
|
||||
min_size: Vec2::ZERO,
|
||||
corner_radius: None,
|
||||
selected: false,
|
||||
@@ -52,6 +54,27 @@ impl<'a> Button<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a selectable button.
|
||||
///
|
||||
/// Equivalent to:
|
||||
/// ```rust
|
||||
/// # use egui::{Button, IntoAtoms, __run_test_ui};
|
||||
/// # __run_test_ui(|ui| {
|
||||
/// let selected = true;
|
||||
/// ui.add(Button::new("toggle me").selected(selected).frame_when_inactive(!selected).frame(true));
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
/// - [`Ui::selectable_value`]
|
||||
/// - [`Ui::selectable_label`]
|
||||
pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self {
|
||||
Self::new(atoms)
|
||||
.selected(selected)
|
||||
.frame_when_inactive(selected)
|
||||
.frame(true)
|
||||
}
|
||||
|
||||
/// Creates a button with an image. The size of the image as displayed is defined by the provided size.
|
||||
///
|
||||
/// Note: In contrast to [`Button::new`], this limits the image size to the default font height
|
||||
@@ -138,6 +161,18 @@ impl<'a> Button<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// If `false`, the button will not have a frame when inactive.
|
||||
///
|
||||
/// Default: `true`.
|
||||
///
|
||||
/// Note: When [`Self::frame`] (or `ui.visuals().button_frame`) is `false`, this setting
|
||||
/// has no effect.
|
||||
#[inline]
|
||||
pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
|
||||
self.frame_when_inactive = frame_when_inactive;
|
||||
self
|
||||
}
|
||||
|
||||
/// By default, buttons senses clicks.
|
||||
/// Change this to a drag-button with `Sense::drag()`.
|
||||
#[inline]
|
||||
@@ -220,6 +255,7 @@ impl<'a> Button<'a> {
|
||||
stroke,
|
||||
small,
|
||||
frame,
|
||||
frame_when_inactive,
|
||||
mut min_size,
|
||||
corner_radius,
|
||||
selected,
|
||||
@@ -243,9 +279,9 @@ impl<'a> Button<'a> {
|
||||
|
||||
let text = layout.text().map(String::from);
|
||||
|
||||
let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
|
||||
let has_frame_margin = frame.unwrap_or_else(|| ui.visuals().button_frame);
|
||||
|
||||
let mut button_padding = if has_frame {
|
||||
let mut button_padding = if has_frame_margin {
|
||||
ui.spacing().button_padding
|
||||
} else {
|
||||
Vec2::ZERO
|
||||
@@ -262,13 +298,22 @@ impl<'a> Button<'a> {
|
||||
let response = if ui.is_rect_visible(prepared.response.rect) {
|
||||
let visuals = ui.style().interact_selectable(&prepared.response, selected);
|
||||
|
||||
let visible_frame = if frame_when_inactive {
|
||||
has_frame_margin
|
||||
} else {
|
||||
has_frame_margin
|
||||
&& (prepared.response.hovered()
|
||||
|| prepared.response.is_pointer_button_down_on()
|
||||
|| prepared.response.has_focus())
|
||||
};
|
||||
|
||||
if image_tint_follows_text_color {
|
||||
prepared.map_images(|image| image.tint(visuals.text_color()));
|
||||
}
|
||||
|
||||
prepared.fallback_text_color = visuals.text_color();
|
||||
|
||||
if has_frame {
|
||||
if visible_frame {
|
||||
let stroke = stroke.unwrap_or(visuals.bg_stroke);
|
||||
let fill = fill.unwrap_or(visuals.weak_bg_fill);
|
||||
prepared.frame = prepared
|
||||
|
||||
@@ -278,7 +278,8 @@ impl<'a> Image<'a> {
|
||||
}
|
||||
|
||||
/// Set alt text for the image. This will be shown when the image fails to load.
|
||||
/// It will also be read to screen readers.
|
||||
///
|
||||
/// It will also be used for accessibility (e.g. read by screen readers).
|
||||
#[inline]
|
||||
pub fn alt_text(mut self, label: impl Into<String>) -> Self {
|
||||
self.alt_text = Some(label.into());
|
||||
@@ -672,7 +673,7 @@ pub fn paint_texture_load_result(
|
||||
rect: Rect,
|
||||
show_loading_spinner: Option<bool>,
|
||||
options: &ImageOptions,
|
||||
alt: Option<&str>,
|
||||
alt_text: Option<&str>,
|
||||
) {
|
||||
match tlr {
|
||||
Ok(TexturePoll::Ready { texture }) => {
|
||||
@@ -697,9 +698,9 @@ pub fn paint_texture_load_result(
|
||||
0.0,
|
||||
TextFormat::simple(font_id.clone(), ui.visuals().error_fg_color),
|
||||
);
|
||||
if let Some(alt) = alt {
|
||||
if let Some(alt_text) = alt_text {
|
||||
job.append(
|
||||
alt,
|
||||
alt_text,
|
||||
ui.spacing().item_spacing.x,
|
||||
TextFormat::simple(font_id, ui.visuals().text_color()),
|
||||
);
|
||||
|
||||
@@ -22,6 +22,8 @@ mod slider;
|
||||
mod spinner;
|
||||
pub mod text_edit;
|
||||
|
||||
#[expect(deprecated)]
|
||||
pub use self::selected_label::SelectableLabel;
|
||||
pub use self::{
|
||||
button::Button,
|
||||
checkbox::Checkbox,
|
||||
@@ -35,7 +37,6 @@ pub use self::{
|
||||
label::Label,
|
||||
progress_bar::ProgressBar,
|
||||
radio_button::RadioButton,
|
||||
selected_label::SelectableLabel,
|
||||
separator::Separator,
|
||||
slider::{Slider, SliderClamping, SliderOrientation},
|
||||
spinner::Spinner,
|
||||
|
||||
@@ -1,88 +1,13 @@
|
||||
use crate::{
|
||||
NumExt as _, Response, Sense, TextStyle, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
|
||||
};
|
||||
#![expect(deprecated, clippy::new_ret_no_self)]
|
||||
|
||||
/// One out of several alternatives, either selected or not.
|
||||
/// Will mark selected items with a different background color.
|
||||
/// An alternative to [`crate::RadioButton`] and [`crate::Checkbox`].
|
||||
///
|
||||
/// Usually you'd use [`Ui::selectable_value`] or [`Ui::selectable_label`] instead.
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// #[derive(PartialEq)]
|
||||
/// enum Enum { First, Second, Third }
|
||||
/// let mut my_enum = Enum::First;
|
||||
///
|
||||
/// ui.selectable_value(&mut my_enum, Enum::First, "First");
|
||||
///
|
||||
/// // is equivalent to:
|
||||
///
|
||||
/// if ui.add(egui::SelectableLabel::new(my_enum == Enum::First, "First")).clicked() {
|
||||
/// my_enum = Enum::First
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
|
||||
pub struct SelectableLabel {
|
||||
selected: bool,
|
||||
text: WidgetText,
|
||||
}
|
||||
use crate::WidgetText;
|
||||
|
||||
#[deprecated = "Use `Button::selectable()` instead"]
|
||||
pub struct SelectableLabel {}
|
||||
|
||||
impl SelectableLabel {
|
||||
pub fn new(selected: bool, text: impl Into<WidgetText>) -> Self {
|
||||
Self {
|
||||
selected,
|
||||
text: text.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for SelectableLabel {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let Self { selected, text } = self;
|
||||
|
||||
let button_padding = ui.spacing().button_padding;
|
||||
let total_extra = button_padding + button_padding;
|
||||
|
||||
let wrap_width = ui.available_width() - total_extra.x;
|
||||
let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button);
|
||||
|
||||
let mut desired_size = total_extra + galley.size();
|
||||
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
||||
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click());
|
||||
response.widget_info(|| {
|
||||
WidgetInfo::selected(
|
||||
WidgetType::SelectableLabel,
|
||||
ui.is_enabled(),
|
||||
selected,
|
||||
galley.text(),
|
||||
)
|
||||
});
|
||||
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
let text_pos = ui
|
||||
.layout()
|
||||
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
|
||||
.min;
|
||||
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
if selected || response.hovered() || response.highlighted() || response.has_focus() {
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
|
||||
ui.painter().rect(
|
||||
rect,
|
||||
visuals.corner_radius,
|
||||
visuals.weak_bg_fill,
|
||||
visuals.bg_stroke,
|
||||
epaint::StrokeKind::Inside,
|
||||
);
|
||||
}
|
||||
|
||||
ui.painter().galley(text_pos, galley, visuals.text_color());
|
||||
}
|
||||
|
||||
response
|
||||
#[deprecated = "Use `Button::selectable()` instead"]
|
||||
pub fn new(selected: bool, text: impl Into<WidgetText>) -> super::Button<'static> {
|
||||
crate::Button::selectable(selected, text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley
|
||||
/// See [`TextEdit::show`].
|
||||
///
|
||||
/// ## Other
|
||||
/// The background color of a [`crate::TextEdit`] is [`crate::Visuals::extreme_bg_color`] or can be set with [`crate::TextEdit::background_color`].
|
||||
/// The background color of a [`crate::TextEdit`] is [`crate::Visuals::text_edit_bg_color`] or can be set with [`crate::TextEdit::background_color`].
|
||||
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
|
||||
pub struct TextEdit<'t> {
|
||||
text: &'t mut dyn TextBuffer,
|
||||
@@ -207,7 +207,7 @@ impl<'t> TextEdit<'t> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the background color of the [`TextEdit`]. The default is [`crate::Visuals::extreme_bg_color`].
|
||||
/// Set the background color of the [`TextEdit`]. The default is [`crate::Visuals::text_edit_bg_color`].
|
||||
// TODO(bircni): remove this once #3284 is implemented
|
||||
#[inline]
|
||||
pub fn background_color(mut self, color: Color32) -> Self {
|
||||
@@ -428,7 +428,7 @@ impl TextEdit<'_> {
|
||||
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||
let background_color = self
|
||||
.background_color
|
||||
.unwrap_or(ui.visuals().extreme_bg_color);
|
||||
.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
|
||||
let output = self.show_content(ui);
|
||||
|
||||
if frame {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8cf6d0b20f127f22d49daefed27fc2d0ca43d645fe1486cf7f6fcbb676bdec82
|
||||
size 179065
|
||||
oid sha256:fc3dbdcd483d4da7a9c1a00f0245a7882997fbcd2d26f8d6a6d2d855f3382063
|
||||
size 179724
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0e37b3ce49c9ccc1a64beb58b176e23ab6c1fa2d897f676b0de85e510e6bfa85
|
||||
size 100845
|
||||
oid sha256:c8ad2c2d494e2287b878049091688069e4d86b69ae72b89cb7ecbe47d8c35e33
|
||||
size 100766
|
||||
|
||||
@@ -69,6 +69,6 @@ fn test_demo_app() {
|
||||
// Can't use Harness::run because fractal clock keeps requesting repaints
|
||||
harness.run_steps(4);
|
||||
|
||||
results.add(harness.try_snapshot(&anchor.to_string()));
|
||||
results.add(harness.try_snapshot(anchor.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
let fonts = egui::epaint::text::Fonts::new(
|
||||
pixels_per_point,
|
||||
max_texture_side,
|
||||
egui::epaint::AlphaFromCoverage::default(),
|
||||
egui::FontDefinitions::default(),
|
||||
);
|
||||
{
|
||||
@@ -210,7 +211,11 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
|
||||
let mut rng = rand::rng();
|
||||
b.iter(|| {
|
||||
fonts.begin_pass(pixels_per_point, max_texture_side);
|
||||
fonts.begin_pass(
|
||||
pixels_per_point,
|
||||
max_texture_side,
|
||||
egui::epaint::AlphaFromCoverage::default(),
|
||||
);
|
||||
|
||||
// Delete a random character, simulating a user making an edit in a long file:
|
||||
let mut new_string = string.clone();
|
||||
|
||||
BIN
crates/egui_demo_lib/data/ring.png
Normal file
BIN
crates/egui_demo_lib/data/ring.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 507 B |
@@ -373,7 +373,7 @@ mod tests {
|
||||
use crate::{Demo as _, demo::demo_app_windows::DemoGroups};
|
||||
|
||||
use egui_kittest::kittest::{NodeT as _, Queryable as _};
|
||||
use egui_kittest::{Harness, SnapshotOptions, SnapshotResults};
|
||||
use egui_kittest::{Harness, OsThreshold, SnapshotOptions, SnapshotResults};
|
||||
|
||||
#[test]
|
||||
fn demos_should_match_snapshot() {
|
||||
@@ -410,12 +410,13 @@ mod tests {
|
||||
harness.run_ok();
|
||||
|
||||
let mut options = SnapshotOptions::default();
|
||||
// The Bézier Curve demo needs a threshold of 2.1 to pass on linux
|
||||
|
||||
if name == "Bézier Curve" {
|
||||
options.threshold = 2.1;
|
||||
// The Bézier Curve demo needs a threshold of 2.1 to pass on linux:
|
||||
options = options.threshold(OsThreshold::new(0.0).linux(2.1));
|
||||
}
|
||||
|
||||
results.add(harness.try_snapshot_options(&format!("demos/{name}"), &options));
|
||||
results.add(harness.try_snapshot_options(format!("demos/{name}"), &options));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
|
||||
let result = ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Toggle the `show_plaintext` bool with a button:
|
||||
let response = ui
|
||||
.add(egui::SelectableLabel::new(show_plaintext, "👁"))
|
||||
.selectable_label(show_plaintext, "👁")
|
||||
.on_hover_text("Show/hide password");
|
||||
|
||||
if response.clicked() {
|
||||
|
||||
@@ -374,7 +374,7 @@ mod tests {
|
||||
harness.fit_contents();
|
||||
harness.run();
|
||||
|
||||
harness.snapshot(&format!("tessellation_test/{name}"));
|
||||
harness.snapshot(format!("tessellation_test/{name}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,16 +319,27 @@ mod tests {
|
||||
date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(2.0)
|
||||
.with_size(Vec2::new(380.0, 550.0))
|
||||
.build_ui(|ui| {
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
demo.ui(ui);
|
||||
});
|
||||
|
||||
harness.fit_contents();
|
||||
for pixels_per_point in [1, 2] {
|
||||
for theme in [egui::Theme::Light, egui::Theme::Dark] {
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(pixels_per_point as f32)
|
||||
.with_theme(theme)
|
||||
.with_size(Vec2::new(380.0, 550.0))
|
||||
.build_ui(|ui| {
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
demo.ui(ui);
|
||||
});
|
||||
|
||||
harness.snapshot("widget_gallery");
|
||||
harness.fit_contents();
|
||||
|
||||
let theme_name = match theme {
|
||||
egui::Theme::Light => "light",
|
||||
egui::Theme::Dark => "dark",
|
||||
};
|
||||
let image_name = format!("widget_gallery_{theme_name}_x{pixels_per_point}");
|
||||
harness.snapshot(&image_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ impl ColorTest {
|
||||
ui.separator();
|
||||
|
||||
// TODO(emilk): test color multiplication (image tint),
|
||||
// to make sure vertex and texture color multiplication is done in linear space.
|
||||
// to make sure vertex and texture color multiplication is done in gamma space.
|
||||
|
||||
ui.label("Gamma interpolation:");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma);
|
||||
@@ -191,8 +191,8 @@ impl ColorTest {
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Linear interpolation (texture sampling):");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear);
|
||||
ui.label("Texture interpolation (texture sampling) should be in gamma space:");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma);
|
||||
}
|
||||
|
||||
fn show_gradients(
|
||||
@@ -245,11 +245,10 @@ impl ColorTest {
|
||||
let g = Gradient::endpoints(left, right);
|
||||
|
||||
match interpolation {
|
||||
Interpolation::Linear => {
|
||||
// texture sampler is sRGBA aware, and should therefore be linear
|
||||
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
|
||||
}
|
||||
Interpolation::Linear => {}
|
||||
Interpolation::Gamma => {
|
||||
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
|
||||
|
||||
// vertex shader uses gamma
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
@@ -330,7 +329,10 @@ fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Respon
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Interpolation {
|
||||
/// egui used to want Linear interpolation for some things, but now we're always in gamma space.
|
||||
#[expect(unused)]
|
||||
Linear,
|
||||
|
||||
Gamma,
|
||||
}
|
||||
|
||||
@@ -745,7 +747,7 @@ mod tests {
|
||||
|
||||
harness.fit_contents();
|
||||
|
||||
results.add(harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}")));
|
||||
results.add(harness.try_snapshot(format!("rendering_test/dpi_{dpi:.2}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
crates/egui_demo_lib/tests/image_blending.rs
Normal file
25
crates/egui_demo_lib/tests/image_blending.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use egui::{hex_color, include_image};
|
||||
use egui_kittest::Harness;
|
||||
|
||||
#[test]
|
||||
fn test_image_blending() {
|
||||
for pixels_per_point in [1.0, 2.0] {
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(pixels_per_point)
|
||||
.build_ui(|ui| {
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
egui::Frame::new()
|
||||
.fill(hex_color!("#5981FF"))
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::Image::new(include_image!("../data/ring.png"))
|
||||
.max_height(18.0)
|
||||
.tint(egui::Color32::GRAY),
|
||||
);
|
||||
});
|
||||
});
|
||||
harness.run();
|
||||
harness.fit_contents();
|
||||
harness.snapshot(format!("image_blending/image_x{pixels_per_point}"));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cbe9f58cce2466360b4b93b03afaaee36711b3017ddff1b2b56bfe49ea91a076
|
||||
size 31306
|
||||
oid sha256:13262df01a7f2cd5655b8b0bb9379ae02a851c877314375f047a7d749908125c
|
||||
size 31368
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b4f807098e0bc56eaacabb76d646a76036cc66a7a6e54b1c934fa9fecb5b0170
|
||||
size 26470
|
||||
oid sha256:27d5aa7b7e6bd5f59c1765e98ca4588545284456e4cc255799ea797950e09850
|
||||
size 26461
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a0b999914adab3d44c614bdf3b28abd268a4ff6162c5680b43035b3f71cb69bb
|
||||
size 23999
|
||||
oid sha256:6d5f3129e34e22b15245212904e0a3537a0c7e70f1d35fd3e9c784af707038b5
|
||||
size 24018
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0c975c8b646425878b704f32198286010730746caf5d463ca8cbcfe539922816
|
||||
size 99087
|
||||
oid sha256:5d05c74583024825d82f1fe8dbeb2a793e366016e87a639f51d46945831de82a
|
||||
size 99106
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5fc9e2ec3253a30ac9649995b019b6b23d745dba07a327886f574a15c0e99e84
|
||||
size 50082
|
||||
oid sha256:e0a49139611dd5f4e97874e8f7b0e12b649da5f373ff7ee80a7ff678f7f8ecc7
|
||||
size 50321
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3110fab8444cb41dffe8b27277fa5dafd0d335aaf13dca511bcccc8b53fb25c8
|
||||
size 24046
|
||||
oid sha256:17f7065c47712f140e4a9fd9eed61a7118fe12cd79cf0745642a02921eaa596b
|
||||
size 24065
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:df1e4a1e355100056713e751a8979d4201d0e4aab5513ba2f7a3e4852e1347dd
|
||||
size 264340
|
||||
oid sha256:cfc5dd77728ee0b3d319c5851698305851b6713eb054a6eb5b618e9670f58ae5
|
||||
size 277018
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cdff6256488f3a40c65a3d73c0635377bf661c57927bce4c853b2a5f3b33274e
|
||||
size 35121
|
||||
oid sha256:aabc0e3821a2d9b21708e9b8d9be02ad55055ccabe719a93af921dba2384b4b3
|
||||
size 34297
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4a347875ef98ebbd606774e03baffdb317cb0246882db116fee1aa7685efbb88
|
||||
size 179653
|
||||
oid sha256:1bd15215f3ec1b365b8c51987f629d5653e4f40e84c34756aea0dc863af27c1e
|
||||
size 179906
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f0e3eeca8abb4fba632cef4621d478fb66af1a0f13e099dda9a79420cc2b6301
|
||||
size 115320
|
||||
oid sha256:7e80bf8c79e6e431806c85385a0bd9262796efc0a1e74d431a1b896dde0b8651
|
||||
size 115338
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f90d56d40004f61628e3f66cfac817c426cd18eb4b9c69ea1b3a6fe5e75e3f05
|
||||
size 70354
|
||||
oid sha256:a3f8873c9cfb80ddeb1ccc0fa04c1c84ea936db1685238f5d29ee6e89f55e457
|
||||
size 68814
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c4d6a15094eee5d96a8af5c44ea9d0c962d650ee9b867344c86d1229e526dcb5
|
||||
size 12822
|
||||
oid sha256:26e4828e42f54da24d032f384f8829e42bcebaee072923f277db582f84302911
|
||||
size 12847
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:02abc0cbab97e572218f422f4b167957869d4e2b4b388355444c20148d998015
|
||||
size 35200
|
||||
oid sha256:4a4520aa68d6752992fd2f87090a317e6e5e24b5cdb5ee2e82daf07f9471ca80
|
||||
size 35251
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e057c0bba4ec4c30e890c39153bd6dd17c511f410bfb894e66ef3ef9973d8fd4
|
||||
size 807
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c8b573f58a41efe26a0bf335e27cc123ffd4c13b24576e46d96ddedfed68b606
|
||||
size 2027
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9f6cf5b14056522d06f0cb1e56bafd7e5ab7a9033eb358748d43d748bb0ceef1
|
||||
size 553177
|
||||
oid sha256:39bd11647241521c0ad5c7163a1af4f1aa86792018657091a2d47bb7f2c48b47
|
||||
size 598408
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fd3bd1f64995db34a14dbc860ae8b8e269073ed7b8f10d10ce8f99b613cfc999
|
||||
size 769357
|
||||
oid sha256:080a59163ab60d60738cfab5afac7cfbddfb585d8a0ee7d03540924b458badea
|
||||
size 833822
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f12e6145f3a1c3fda6dede3daeb0e52ed2bffb35531d823133224a477798a14a
|
||||
size 907800
|
||||
oid sha256:216d3d028f48f4bfbd6aca0a25500655d4eb4d5581a387397ed0b048d57fc0c3
|
||||
size 984737
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05bdcfd2c34b6d7badede14f5495dce34e5e9cfe421314f40dcea15e9f865736
|
||||
size 1024735
|
||||
oid sha256:399fc3d64e7ff637425effe6c80d684be1cf8bb9b555458352d0ae6c62bddb5a
|
||||
size 1109505
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8365c89f6b823f01464a9310bab7717bf25305b335cdeecf21711c7dca9f053f
|
||||
size 1140082
|
||||
oid sha256:30ce4874a1adb8acf8c8e404f3e19e683eca3133cdef15befbc50d6c09618094
|
||||
size 1241773
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b38021057ec6b5bb39c41bd4afaf5e9ff38687216d52d5bba8cbf7b6fdfe9a4f
|
||||
size 1291518
|
||||
oid sha256:135fbe5f4ee485ee671931b64c16410b9073d40bedb23dc2545fc863448e8c63
|
||||
size 1398091
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ac90da596084a880487035b276177e98d711854143373d59860f01733b1c0cd
|
||||
size 45592
|
||||
oid sha256:1b0fe7aa33506c59142aff764c6b229e8c55b35c8038312b22ea2659987a081a
|
||||
size 45578
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e412d424aac7b9cbdfdb8e36bd598e6cbc77183da7733c94c5f20e70699b8b4a
|
||||
size 87263
|
||||
oid sha256:3a3512ea7235640db79e86aa84039621784270201c9213c910f15e7451e5600b
|
||||
size 87336
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:222a32da21c69ee46e847e29fb05fd5e1d2de6bb7a22358549bc426f8243fdcb
|
||||
size 119671
|
||||
oid sha256:dc4918a534f26b72d42ef20221e26c0f91a0252870a1987c1fe4cc4aa3c74872
|
||||
size 119406
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d42e11f50a9522dd5ae73e8f8336bfb01493751705055a63abea3f5258f7c9c1
|
||||
size 51626
|
||||
oid sha256:71182570a65693839fd7cd7390025731ab3f3f88ab55bc67d8be6466fe5a2c11
|
||||
size 51843
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b567d4038fd73986c80d2bd12197a6df037fde043545993fa9fe4160d0af446c
|
||||
size 54829
|
||||
oid sha256:a0dc0294f990730da34fcbbc53f44280306ec6179826c20a6c9ee946e1148b61
|
||||
size 55042
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fbf40a1f56a6e280002719c6556fe477c93fa7fe88d398372ed36efaa1b83a62
|
||||
size 55282
|
||||
oid sha256:3004adfe5a864bdc768ceb42d1f09b6debcaf5413f8fea4d36b0aff99e4584f9
|
||||
size 55511
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:33621731155ebb463fb01ea41ab20272885250efcd7d5c7683c10936b296e14d
|
||||
size 36446
|
||||
oid sha256:b99360833f59a212a965a13d52485ab8ad0e6420b9288b2d6936507067c22a85
|
||||
size 36395
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:186bd8a3146ad8f1977955e3f7fa593877ad1bf1e8376d32f446c67f36a2aafe
|
||||
size 36493
|
||||
oid sha256:82aa004f668f0ac6b493717b4bff8436ccc1e991c7fb3fcde5b5f3a123c06b9f
|
||||
size 36428
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee129f0542f21e12f5aa3c2f9746e7cadd73441a04d580f57c12c1cdd40d8b07
|
||||
size 153136
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e21bb01ae6e4226402a97b7086b49604cdde6b41a6770199df68dc940cd9a45
|
||||
size 64748
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0626bc45888ad250bf4b49c7f7f462a93ab91e3a2817fd7d0902411043c97132
|
||||
size 153289
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:919a82c95468300bcd09471eb31d53d25d50cdcb02c27ddbc759d24e65da92b6
|
||||
size 59398
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a55e39a640b0e2cc992286a86dcf38460f1abcc7b964df9022549ca1a94c4df5
|
||||
size 146408
|
||||
@@ -100,9 +100,9 @@ impl BytesLoader for FileLoader {
|
||||
let entry = entry.get_mut();
|
||||
*entry = Poll::Ready(result);
|
||||
ctx.request_repaint();
|
||||
log::trace!("finished loading {uri:?}");
|
||||
log::trace!("Finished loading {uri:?}");
|
||||
} else {
|
||||
log::trace!("cancelled loading {uri:?}\nNote: This can happen if `forget_image` is called while the image is still loading.");
|
||||
log::trace!("Canceled loading {uri:?}\nNote: This can happen if `forget_image` is called while the image is still loading.");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -172,12 +172,7 @@ impl Painter {
|
||||
|
||||
let supported_extensions = gl.supported_extensions();
|
||||
log::trace!("OpenGL extensions: {supported_extensions:?}");
|
||||
let srgb_textures = shader_version == ShaderVersion::Es300 // WebGL2 always support sRGB
|
||||
|| supported_extensions.iter().any(|extension| {
|
||||
// EXT_sRGB, GL_ARB_framebuffer_sRGB, GL_EXT_sRGB, GL_EXT_texture_sRGB_decode, …
|
||||
extension.contains("sRGB")
|
||||
});
|
||||
log::debug!("SRGB texture Support: {:?}", srgb_textures);
|
||||
let srgb_textures = false; // egui wants normal sRGB-unaware textures
|
||||
|
||||
let supports_srgb_framebuffer = !cfg!(target_arch = "wasm32")
|
||||
&& supported_extensions.iter().any(|extension| {
|
||||
@@ -202,11 +197,10 @@ impl Painter {
|
||||
&gl,
|
||||
glow::FRAGMENT_SHADER,
|
||||
&format!(
|
||||
"{}\n#define NEW_SHADER_INTERFACE {}\n#define DITHERING {}\n#define SRGB_TEXTURES {}\n{}\n{}",
|
||||
"{}\n#define NEW_SHADER_INTERFACE {}\n#define DITHERING {}\n{}\n{}",
|
||||
shader_version_declaration,
|
||||
shader_version.is_new_shader_interface() as i32,
|
||||
dithering as i32,
|
||||
srgb_textures as i32,
|
||||
shader_prefix,
|
||||
FRAG_SRC
|
||||
),
|
||||
@@ -534,23 +528,6 @@ impl Painter {
|
||||
|
||||
self.upload_texture_srgb(delta.pos, image.size, delta.options, data);
|
||||
}
|
||||
egui::ImageData::Font(image) => {
|
||||
assert_eq!(
|
||||
image.width() * image.height(),
|
||||
image.pixels.len(),
|
||||
"Mismatch between texture size and texel count"
|
||||
);
|
||||
|
||||
let data: Vec<u8> = {
|
||||
profiling::scope!("font -> sRGBA");
|
||||
image
|
||||
.srgba_pixels(None)
|
||||
.flat_map(|a| a.to_array())
|
||||
.collect()
|
||||
};
|
||||
|
||||
self.upload_texture_srgb(delta.pos, image.size, delta.options, &data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -43,25 +43,8 @@ vec3 dither_interleaved(vec3 rgb, float levels) {
|
||||
return rgb + noise / (levels - 1.0);
|
||||
}
|
||||
|
||||
// 0-1 sRGB gamma from 0-1 linear
|
||||
vec3 srgb_gamma_from_linear(vec3 rgb) {
|
||||
bvec3 cutoff = lessThan(rgb, vec3(0.0031308));
|
||||
vec3 lower = rgb * vec3(12.92);
|
||||
vec3 higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055);
|
||||
return mix(higher, lower, vec3(cutoff));
|
||||
}
|
||||
|
||||
// 0-1 sRGBA gamma from 0-1 linear
|
||||
vec4 srgba_gamma_from_linear(vec4 rgba) {
|
||||
return vec4(srgb_gamma_from_linear(rgba.rgb), rgba.a);
|
||||
}
|
||||
|
||||
void main() {
|
||||
#if SRGB_TEXTURES
|
||||
vec4 texture_in_gamma = srgba_gamma_from_linear(texture2D(u_sampler, v_tc));
|
||||
#else
|
||||
vec4 texture_in_gamma = texture2D(u_sampler, v_tc);
|
||||
#endif
|
||||
|
||||
// We multiply the colors in gamma space, because that's the only way to get text to look right.
|
||||
vec4 frag_color_gamma = v_rgba_in_gamma * texture_in_gamma;
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::marker::PhantomData;
|
||||
pub struct HarnessBuilder<State = ()> {
|
||||
pub(crate) screen_rect: Rect,
|
||||
pub(crate) pixels_per_point: f32,
|
||||
pub(crate) theme: egui::Theme,
|
||||
pub(crate) max_steps: u64,
|
||||
pub(crate) step_dt: f32,
|
||||
pub(crate) state: PhantomData<State>,
|
||||
@@ -19,6 +20,7 @@ impl<State> Default for HarnessBuilder<State> {
|
||||
Self {
|
||||
screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)),
|
||||
pixels_per_point: 1.0,
|
||||
theme: egui::Theme::Dark,
|
||||
state: PhantomData,
|
||||
renderer: Box::new(LazyRenderer::default()),
|
||||
max_steps: 4,
|
||||
@@ -45,6 +47,13 @@ impl<State> HarnessBuilder<State> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the desired theme (dark or light).
|
||||
#[inline]
|
||||
pub fn with_theme(mut self, theme: egui::Theme) -> Self {
|
||||
self.theme = theme;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum number of steps to run when calling [`Harness::run`].
|
||||
///
|
||||
/// Default is 4.
|
||||
|
||||
@@ -28,6 +28,7 @@ pub use builder::*;
|
||||
pub use node::*;
|
||||
pub use renderer::*;
|
||||
|
||||
use egui::style::ScrollAnimation;
|
||||
use egui::{Key, Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId};
|
||||
use kittest::Queryable;
|
||||
|
||||
@@ -55,6 +56,10 @@ impl Display for ExceededMaxStepsError {
|
||||
/// The [Harness] has a optional generic state that can be used to pass data to the app / ui closure.
|
||||
/// In _most cases_ it should be fine to just store the state in the closure itself.
|
||||
/// The state functions are useful if you need to access the state after the harness has been created.
|
||||
///
|
||||
/// Some egui style options are changed from the defaults:
|
||||
/// - The cursor blinking is disabled
|
||||
/// - The scroll animation is disabled
|
||||
pub struct Harness<'a, State = ()> {
|
||||
pub ctx: egui::Context,
|
||||
input: egui::RawInput,
|
||||
@@ -86,6 +91,7 @@ impl<'a, State> Harness<'a, State> {
|
||||
let HarnessBuilder {
|
||||
screen_rect,
|
||||
pixels_per_point,
|
||||
theme,
|
||||
max_steps,
|
||||
step_dt,
|
||||
state: _,
|
||||
@@ -93,9 +99,14 @@ impl<'a, State> Harness<'a, State> {
|
||||
wait_for_pending_images,
|
||||
} = builder;
|
||||
let ctx = ctx.unwrap_or_default();
|
||||
ctx.set_theme(theme);
|
||||
ctx.enable_accesskit();
|
||||
// Disable cursor blinking so it doesn't interfere with snapshots
|
||||
ctx.all_styles_mut(|style| style.visuals.text_cursor.blink = false);
|
||||
ctx.all_styles_mut(|style| {
|
||||
// Disable cursor blinking so it doesn't interfere with snapshots
|
||||
style.visuals.text_cursor.blink = false;
|
||||
style.scroll_animation = ScrollAnimation::none();
|
||||
style.animation_time = 0.0;
|
||||
});
|
||||
let mut input = egui::RawInput {
|
||||
screen_rect: Some(screen_rect),
|
||||
..Default::default()
|
||||
@@ -562,7 +573,8 @@ impl<'a, State> Harness<'a, State> {
|
||||
.expect("Missing root viewport")
|
||||
}
|
||||
|
||||
fn root(&self) -> Node<'_> {
|
||||
/// The root node of the test harness.
|
||||
pub fn root(&self) -> Node<'_> {
|
||||
Node {
|
||||
accesskit_node: self.kittest.root(),
|
||||
queue: &self.queued_events,
|
||||
|
||||
@@ -159,4 +159,49 @@ impl Node<'_> {
|
||||
pub fn is_focused(&self) -> bool {
|
||||
self.accesskit_node.is_focused()
|
||||
}
|
||||
|
||||
/// Scroll the node into view.
|
||||
pub fn scroll_to_me(&self) {
|
||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||
action: accesskit::Action::ScrollIntoView,
|
||||
target: self.accesskit_node.id(),
|
||||
data: None,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Scroll the [`egui::ScrollArea`] containing this node down (100px).
|
||||
pub fn scroll_down(&self) {
|
||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||
action: accesskit::Action::ScrollDown,
|
||||
target: self.accesskit_node.id(),
|
||||
data: None,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Scroll the [`egui::ScrollArea`] containing this node up (100px).
|
||||
pub fn scroll_up(&self) {
|
||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||
action: accesskit::Action::ScrollUp,
|
||||
target: self.accesskit_node.id(),
|
||||
data: None,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Scroll the [`egui::ScrollArea`] containing this node left (100px).
|
||||
pub fn scroll_left(&self) {
|
||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||
action: accesskit::Action::ScrollLeft,
|
||||
target: self.accesskit_node.id(),
|
||||
data: None,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Scroll the [`egui::ScrollArea`] containing this node right (100px).
|
||||
pub fn scroll_right(&self) {
|
||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||
action: accesskit::Action::ScrollRight,
|
||||
target: self.accesskit_node.id(),
|
||||
data: None,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,117 @@ pub struct SnapshotOptions {
|
||||
/// wgpu backends).
|
||||
pub threshold: f32,
|
||||
|
||||
/// The number of pixels that can differ before the snapshot is considered a failure.
|
||||
/// Preferably, you should use `threshold` to control the sensitivity of the image comparison.
|
||||
/// As a last resort, you can use this to allow a certain number of pixels to differ.
|
||||
/// If `None`, the default is `0` (meaning no pixels can differ).
|
||||
/// If `Some`, the value can be set per OS
|
||||
pub failed_pixel_count_threshold: usize,
|
||||
|
||||
/// The path where the snapshots will be saved.
|
||||
/// The default is `tests/snapshots`.
|
||||
pub output_path: PathBuf,
|
||||
}
|
||||
|
||||
/// Helper struct to define the number of pixels that can differ before the snapshot is considered a failure.
|
||||
/// This is useful if you want to set different thresholds for different operating systems.
|
||||
///
|
||||
/// The default values are 0 / 0.0
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```no_run
|
||||
/// use egui_kittest::{OsThreshold, SnapshotOptions};
|
||||
/// let mut harness = egui_kittest::Harness::new_ui(|ui| {
|
||||
/// ui.label("Hi!");
|
||||
/// });
|
||||
/// harness.snapshot_options(
|
||||
/// "os_threshold_example",
|
||||
/// &SnapshotOptions::new()
|
||||
/// .threshold(OsThreshold::new(0.0).windows(10.0))
|
||||
/// .failed_pixel_count_threshold(OsThreshold::new(0).windows(10).macos(53)
|
||||
/// ))
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct OsThreshold<T> {
|
||||
pub windows: T,
|
||||
pub macos: T,
|
||||
pub linux: T,
|
||||
pub fallback: T,
|
||||
}
|
||||
|
||||
impl From<usize> for OsThreshold<usize> {
|
||||
fn from(value: usize) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> OsThreshold<T>
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
/// Use the same value for all
|
||||
pub fn new(same: T) -> Self {
|
||||
Self {
|
||||
windows: same,
|
||||
macos: same,
|
||||
linux: same,
|
||||
fallback: same,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the threshold for Windows.
|
||||
#[inline]
|
||||
pub fn windows(mut self, threshold: T) -> Self {
|
||||
self.windows = threshold;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the threshold for macOS.
|
||||
#[inline]
|
||||
pub fn macos(mut self, threshold: T) -> Self {
|
||||
self.macos = threshold;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the threshold for Linux.
|
||||
#[inline]
|
||||
pub fn linux(mut self, threshold: T) -> Self {
|
||||
self.linux = threshold;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the threshold for the current operating system.
|
||||
pub fn threshold(&self) -> T {
|
||||
if cfg!(target_os = "windows") {
|
||||
self.windows
|
||||
} else if cfg!(target_os = "macos") {
|
||||
self.macos
|
||||
} else if cfg!(target_os = "linux") {
|
||||
self.linux
|
||||
} else {
|
||||
self.fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OsThreshold<Self>> for usize {
|
||||
fn from(threshold: OsThreshold<Self>) -> Self {
|
||||
threshold.threshold()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OsThreshold<Self>> for f32 {
|
||||
fn from(threshold: OsThreshold<Self>) -> Self {
|
||||
threshold.threshold()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SnapshotOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
threshold: 0.6,
|
||||
output_path: PathBuf::from("tests/snapshots"),
|
||||
failed_pixel_count_threshold: 0, // Default is 0, meaning no pixels can differ
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,8 +138,8 @@ impl SnapshotOptions {
|
||||
/// The default is `0.6` (which is enough for most egui tests to pass across different
|
||||
/// wgpu backends).
|
||||
#[inline]
|
||||
pub fn threshold(mut self, threshold: f32) -> Self {
|
||||
self.threshold = threshold;
|
||||
pub fn threshold(mut self, threshold: impl Into<f32>) -> Self {
|
||||
self.threshold = threshold.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -49,6 +150,20 @@ impl SnapshotOptions {
|
||||
self.output_path = output_path.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the number of pixels that can differ before the snapshot is considered a failure.
|
||||
///
|
||||
/// Preferably, you should use [`Self::threshold`] to control the sensitivity of the image comparison.
|
||||
/// As a last resort, you can use this to allow a certain number of pixels to differ.
|
||||
#[inline]
|
||||
pub fn failed_pixel_count_threshold(
|
||||
mut self,
|
||||
failed_pixel_count_threshold: impl Into<OsThreshold<usize>>,
|
||||
) -> Self {
|
||||
let failed_pixel_count_threshold = failed_pixel_count_threshold.into().threshold();
|
||||
self.failed_pixel_count_threshold = failed_pixel_count_threshold;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -58,7 +173,7 @@ pub enum SnapshotError {
|
||||
/// Name of the test
|
||||
name: String,
|
||||
|
||||
/// Count of pixels that were different
|
||||
/// Count of pixels that were different (above the per-pixel threshold).
|
||||
diff: i32,
|
||||
|
||||
/// Path where the diff image was saved
|
||||
@@ -195,15 +310,24 @@ fn should_update_snapshots() -> bool {
|
||||
/// reading or writing the snapshot.
|
||||
pub fn try_image_snapshot_options(
|
||||
new: &image::RgbaImage,
|
||||
name: &str,
|
||||
name: impl Into<String>,
|
||||
options: &SnapshotOptions,
|
||||
) -> SnapshotResult {
|
||||
try_image_snapshot_options_impl(new, name.into(), options)
|
||||
}
|
||||
|
||||
fn try_image_snapshot_options_impl(
|
||||
new: &image::RgbaImage,
|
||||
name: String,
|
||||
options: &SnapshotOptions,
|
||||
) -> SnapshotResult {
|
||||
let SnapshotOptions {
|
||||
threshold,
|
||||
output_path,
|
||||
failed_pixel_count_threshold,
|
||||
} = options;
|
||||
|
||||
let parent_path = if let Some(parent) = PathBuf::from(name).parent() {
|
||||
let parent_path = if let Some(parent) = PathBuf::from(&name).parent() {
|
||||
output_path.join(parent)
|
||||
} else {
|
||||
output_path.clone()
|
||||
@@ -269,7 +393,7 @@ pub fn try_image_snapshot_options(
|
||||
return update_snapshot();
|
||||
} else {
|
||||
return Err(SnapshotError::SizeMismatch {
|
||||
name: name.to_owned(),
|
||||
name,
|
||||
expected: previous.dimensions(),
|
||||
actual: new.dimensions(),
|
||||
});
|
||||
@@ -280,19 +404,24 @@ pub fn try_image_snapshot_options(
|
||||
let result =
|
||||
dify::diff::get_results(previous, new.clone(), *threshold, true, None, &None, &None);
|
||||
|
||||
if let Some((diff, result_image)) = result {
|
||||
if let Some((num_wrong_pixels, result_image)) = result {
|
||||
result_image
|
||||
.save(diff_path.clone())
|
||||
.map_err(|err| SnapshotError::WriteSnapshot {
|
||||
path: diff_path.clone(),
|
||||
err,
|
||||
})?;
|
||||
|
||||
if should_update_snapshots() {
|
||||
update_snapshot()
|
||||
} else {
|
||||
if num_wrong_pixels as i64 <= *failed_pixel_count_threshold as i64 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(SnapshotError::Diff {
|
||||
name: name.to_owned(),
|
||||
diff,
|
||||
name,
|
||||
diff: num_wrong_pixels,
|
||||
diff_path,
|
||||
})
|
||||
}
|
||||
@@ -314,7 +443,7 @@ pub fn try_image_snapshot_options(
|
||||
/// # Errors
|
||||
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
|
||||
/// reading or writing the snapshot.
|
||||
pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> SnapshotResult {
|
||||
pub fn try_image_snapshot(current: &image::RgbaImage, name: impl Into<String>) -> SnapshotResult {
|
||||
try_image_snapshot_options(current, name, &SnapshotOptions::default())
|
||||
}
|
||||
|
||||
@@ -335,7 +464,11 @@ pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> SnapshotRes
|
||||
/// Panics if the image does not match the snapshot or if there was an error reading or writing the
|
||||
/// snapshot.
|
||||
#[track_caller]
|
||||
pub fn image_snapshot_options(current: &image::RgbaImage, name: &str, options: &SnapshotOptions) {
|
||||
pub fn image_snapshot_options(
|
||||
current: &image::RgbaImage,
|
||||
name: impl Into<String>,
|
||||
options: &SnapshotOptions,
|
||||
) {
|
||||
match try_image_snapshot_options(current, name, options) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
@@ -354,7 +487,7 @@ pub fn image_snapshot_options(current: &image::RgbaImage, name: &str, options: &
|
||||
/// Panics if the image does not match the snapshot or if there was an error reading or writing the
|
||||
/// snapshot.
|
||||
#[track_caller]
|
||||
pub fn image_snapshot(current: &image::RgbaImage, name: &str) {
|
||||
pub fn image_snapshot(current: &image::RgbaImage, name: impl Into<String>) {
|
||||
match try_image_snapshot(current, name) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
@@ -385,13 +518,13 @@ impl<State> Harness<'_, State> {
|
||||
/// error reading or writing the snapshot, if the rendering fails or if no default renderer is available.
|
||||
pub fn try_snapshot_options(
|
||||
&mut self,
|
||||
name: &str,
|
||||
name: impl Into<String>,
|
||||
options: &SnapshotOptions,
|
||||
) -> SnapshotResult {
|
||||
let image = self
|
||||
.render()
|
||||
.map_err(|err| SnapshotError::RenderError { err })?;
|
||||
try_image_snapshot_options(&image, name, options)
|
||||
try_image_snapshot_options(&image, name.into(), options)
|
||||
}
|
||||
|
||||
/// Render an image using the setup [`crate::TestRenderer`] and compare it to the snapshot.
|
||||
@@ -402,7 +535,7 @@ impl<State> Harness<'_, State> {
|
||||
/// # Errors
|
||||
/// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an
|
||||
/// error reading or writing the snapshot, if the rendering fails or if no default renderer is available.
|
||||
pub fn try_snapshot(&mut self, name: &str) -> SnapshotResult {
|
||||
pub fn try_snapshot(&mut self, name: impl Into<String>) -> SnapshotResult {
|
||||
let image = self
|
||||
.render()
|
||||
.map_err(|err| SnapshotError::RenderError { err })?;
|
||||
@@ -428,7 +561,7 @@ impl<State> Harness<'_, State> {
|
||||
/// Panics if the image does not match the snapshot, if there was an error reading or writing the
|
||||
/// snapshot, if the rendering fails or if no default renderer is available.
|
||||
#[track_caller]
|
||||
pub fn snapshot_options(&mut self, name: &str, options: &SnapshotOptions) {
|
||||
pub fn snapshot_options(&mut self, name: impl Into<String>, options: &SnapshotOptions) {
|
||||
match self.try_snapshot_options(name, options) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
@@ -446,7 +579,7 @@ impl<State> Harness<'_, State> {
|
||||
/// Panics if the image does not match the snapshot, if there was an error reading or writing the
|
||||
/// snapshot, if the rendering fails or if no default renderer is available.
|
||||
#[track_caller]
|
||||
pub fn snapshot(&mut self, name: &str) {
|
||||
pub fn snapshot(&mut self, name: impl Into<String>) {
|
||||
match self.try_snapshot(name) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
@@ -467,7 +600,7 @@ impl<State> Harness<'_, State> {
|
||||
)]
|
||||
pub fn try_wgpu_snapshot_options(
|
||||
&mut self,
|
||||
name: &str,
|
||||
name: impl Into<String>,
|
||||
options: &SnapshotOptions,
|
||||
) -> SnapshotResult {
|
||||
self.try_snapshot_options(name, options)
|
||||
@@ -477,7 +610,7 @@ impl<State> Harness<'_, State> {
|
||||
since = "0.31.0",
|
||||
note = "Use `try_snapshot` instead. This function will be removed in 0.32"
|
||||
)]
|
||||
pub fn try_wgpu_snapshot(&mut self, name: &str) -> SnapshotResult {
|
||||
pub fn try_wgpu_snapshot(&mut self, name: impl Into<String>) -> SnapshotResult {
|
||||
self.try_snapshot(name)
|
||||
}
|
||||
|
||||
@@ -485,7 +618,7 @@ impl<State> Harness<'_, State> {
|
||||
since = "0.31.0",
|
||||
note = "Use `snapshot_options` instead. This function will be removed in 0.32"
|
||||
)]
|
||||
pub fn wgpu_snapshot_options(&mut self, name: &str, options: &SnapshotOptions) {
|
||||
pub fn wgpu_snapshot_options(&mut self, name: impl Into<String>, options: &SnapshotOptions) {
|
||||
self.snapshot_options(name, options);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:70d76e55327de17163bc9c7e128c28153f95db3229dec919352a024eb80544f1
|
||||
size 7399
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b4b7b3145401b7cf9815a652a0914b230892ffda3b5e23fea530dafee9c0c3d3
|
||||
size 8110
|
||||
@@ -1,5 +1,5 @@
|
||||
use egui::{Modifiers, Vec2, include_image};
|
||||
use egui_kittest::Harness;
|
||||
use egui::{Modifiers, ScrollArea, Vec2, include_image};
|
||||
use egui_kittest::{Harness, SnapshotResults};
|
||||
use kittest::Queryable as _;
|
||||
|
||||
#[test]
|
||||
@@ -81,3 +81,60 @@ fn should_wait_for_images() {
|
||||
|
||||
harness.snapshot("should_wait_for_images");
|
||||
}
|
||||
|
||||
fn test_scroll_harness() -> Harness<'static, bool> {
|
||||
Harness::builder()
|
||||
.with_size(Vec2::new(100.0, 200.0))
|
||||
.build_ui_state(
|
||||
|ui, state| {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
for i in 0..20 {
|
||||
ui.label(format!("Item {i}"));
|
||||
}
|
||||
if ui.button("Hidden Button").clicked() {
|
||||
*state = true;
|
||||
};
|
||||
});
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scroll_to_me() {
|
||||
let mut harness = test_scroll_harness();
|
||||
let mut results = SnapshotResults::new();
|
||||
|
||||
results.add(harness.try_snapshot("test_scroll_initial"));
|
||||
|
||||
harness.get_by_label("Hidden Button").scroll_to_me();
|
||||
|
||||
harness.run();
|
||||
results.add(harness.try_snapshot("test_scroll_scrolled"));
|
||||
|
||||
harness.get_by_label("Hidden Button").click();
|
||||
harness.run();
|
||||
|
||||
assert!(
|
||||
harness.state(),
|
||||
"The button was not clicked after scrolling."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scroll_down() {
|
||||
let mut harness = test_scroll_harness();
|
||||
|
||||
let button = harness.get_by_label("Hidden Button");
|
||||
button.scroll_down();
|
||||
button.scroll_down();
|
||||
harness.run();
|
||||
|
||||
harness.get_by_label("Hidden Button").click();
|
||||
harness.run();
|
||||
|
||||
assert!(
|
||||
harness.state(),
|
||||
"The button was not clicked after scrolling down. (Probably not scrolled enough / at all)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ impl Align {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Two-dimension alignment, e.g. [`Align2::LEFT_TOP`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Align2(pub [Align; 2]);
|
||||
|
||||
@@ -298,3 +298,9 @@ impl std::ops::IndexMut<usize> for Align2 {
|
||||
pub fn center_size_in_rect(size: Vec2, frame: Rect) -> Rect {
|
||||
Align2::CENTER_CENTER.align_size_within_rect(size, frame)
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Align2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Align2({:?}, {:?})", self.x(), self.y())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::{Div, Mul, Pos2, Rangef, Rot2, Vec2, lerp, pos2, vec2};
|
||||
use crate::{Div, Mul, NumExt as _, Pos2, Rangef, Rot2, Vec2, lerp, pos2, vec2};
|
||||
|
||||
/// A rectangular region of space.
|
||||
///
|
||||
@@ -341,11 +341,13 @@ impl Rect {
|
||||
self.max - self.min
|
||||
}
|
||||
|
||||
/// Note: this can be negative.
|
||||
#[inline(always)]
|
||||
pub fn width(&self) -> f32 {
|
||||
self.max.x - self.min.x
|
||||
}
|
||||
|
||||
/// Note: this can be negative.
|
||||
#[inline(always)]
|
||||
pub fn height(&self) -> f32 {
|
||||
self.max.y - self.min.y
|
||||
@@ -373,9 +375,10 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// This is never negative, and instead returns zero for negative rectangles.
|
||||
#[inline(always)]
|
||||
pub fn area(&self) -> f32 {
|
||||
self.width() * self.height()
|
||||
self.width().at_least(0.0) * self.height().at_least(0.0)
|
||||
}
|
||||
|
||||
/// The distance from the rect to the position.
|
||||
@@ -651,7 +654,7 @@ impl Rect {
|
||||
pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool {
|
||||
debug_assert!(
|
||||
d.is_normalized(),
|
||||
"expected normalized direction, but `d` has length {}",
|
||||
"Debug assert: expected normalized direction, but `d` has length {}",
|
||||
d.length()
|
||||
);
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ use crate::{Align2, Pos2, Rect, Vec2};
|
||||
///
|
||||
/// There are helper constants for the 12 common menu positions:
|
||||
/// ```text
|
||||
/// ┌───────────┐ ┌────────┐ ┌─────────┐
|
||||
/// │ TOP_START │ │ TOP │ │ TOP_END │
|
||||
/// └───────────┘ └────────┘ └─────────┘
|
||||
/// ┌───────────┐ ┌────────┐ ┌─────────┐
|
||||
/// │ TOP_START │ │ TOP │ │ TOP_END │
|
||||
/// └───────────┘ └────────┘ └─────────┘
|
||||
/// ┌──────────┐ ┌────────────────────────────────────┐ ┌───────────┐
|
||||
/// │LEFT_START│ │ │ │RIGHT_START│
|
||||
/// └──────────┘ │ │ └───────────┘
|
||||
@@ -19,9 +19,9 @@ use crate::{Align2, Pos2, Rect, Vec2};
|
||||
/// ┌──────────┐ │ │ ┌───────────┐
|
||||
/// │ LEFT_END │ │ │ │ RIGHT_END │
|
||||
/// └──────────┘ └────────────────────────────────────┘ └───────────┘
|
||||
/// ┌────────────┐ ┌──────┐ ┌──────────┐
|
||||
/// │BOTTOM_START│ │BOTTOM│ │BOTTOM_END│
|
||||
/// └────────────┘ └──────┘ └──────────┘
|
||||
/// ┌────────────┐ ┌──────┐ ┌──────────┐
|
||||
/// │BOTTOM_START│ │BOTTOM│ │BOTTOM_END│
|
||||
/// └────────────┘ └──────┘ └──────────┘
|
||||
/// ```
|
||||
// There is no `new` function on purpose, since writing out `parent` and `child` is more
|
||||
// reasonable.
|
||||
@@ -235,45 +235,34 @@ impl RectAlign {
|
||||
[self.flip_x(), self.flip_y(), self.flip()]
|
||||
}
|
||||
|
||||
/// Look for the [`RectAlign`] that fits best in the available space.
|
||||
/// Look for the first alternative [`RectAlign`] that allows the child rect to fit
|
||||
/// inside the `screen_rect`.
|
||||
///
|
||||
/// If no alternative fits, the first is returned.
|
||||
/// If no alternatives are given, `None` is returned.
|
||||
///
|
||||
/// See also:
|
||||
/// - [`RectAlign::symmetries`] to calculate alternatives
|
||||
/// - [`RectAlign::MENU_ALIGNS`] for the 12 common menu positions
|
||||
pub fn find_best_align(
|
||||
mut values_to_try: impl Iterator<Item = Self>,
|
||||
available_space: Rect,
|
||||
values_to_try: impl Iterator<Item = Self>,
|
||||
screen_rect: Rect,
|
||||
parent_rect: Rect,
|
||||
gap: f32,
|
||||
size: Vec2,
|
||||
) -> Self {
|
||||
let area = size.x * size.y;
|
||||
|
||||
let blocked_area = |pos: Self| {
|
||||
let rect = pos.align_rect(&parent_rect, size, gap);
|
||||
area - available_space.intersect(rect).area()
|
||||
};
|
||||
|
||||
let first = values_to_try.next().unwrap_or_default();
|
||||
|
||||
if blocked_area(first) == 0.0 {
|
||||
return first;
|
||||
}
|
||||
|
||||
let mut best_area = blocked_area(first);
|
||||
let mut best = first;
|
||||
expected_size: Vec2,
|
||||
) -> Option<Self> {
|
||||
let mut first_choice = None;
|
||||
|
||||
for align in values_to_try {
|
||||
let blocked = blocked_area(align);
|
||||
if blocked == 0.0 {
|
||||
return align;
|
||||
}
|
||||
if blocked < best_area {
|
||||
best = align;
|
||||
best_area = blocked;
|
||||
first_choice = first_choice.or(Some(align)); // Remember the first alternative
|
||||
|
||||
let suggested_popup_rect = align.align_rect(&parent_rect, expected_size, gap);
|
||||
|
||||
if screen_rect.contains_rect(suggested_popup_rect) {
|
||||
return Some(align);
|
||||
}
|
||||
}
|
||||
|
||||
best
|
||||
first_choice
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use criterion::{Criterion, black_box, criterion_group, criterion_main};
|
||||
|
||||
use epaint::{
|
||||
ClippedShape, Color32, Mesh, PathStroke, Pos2, Rect, Shape, Stroke, TessellationOptions,
|
||||
Tessellator, TextureAtlas, Vec2, pos2, tessellator::Path,
|
||||
AlphaFromCoverage, ClippedShape, Color32, Mesh, PathStroke, Pos2, Rect, Shape, Stroke,
|
||||
TessellationOptions, Tessellator, TextureAtlas, Vec2, pos2, tessellator::Path,
|
||||
};
|
||||
|
||||
#[global_allocator]
|
||||
@@ -66,7 +66,7 @@ fn tessellate_circles(c: &mut Criterion) {
|
||||
let pixels_per_point = 2.0;
|
||||
let options = TessellationOptions::default();
|
||||
|
||||
let atlas = TextureAtlas::new([4096, 256]);
|
||||
let atlas = TextureAtlas::new([4096, 256], AlphaFromCoverage::default());
|
||||
let font_tex_size = atlas.size();
|
||||
let prepared_discs = atlas.prepared_discs();
|
||||
|
||||
|
||||
@@ -7,24 +7,20 @@ use std::sync::Arc;
|
||||
///
|
||||
/// To load an image file, see [`ColorImage::from_rgba_unmultiplied`].
|
||||
///
|
||||
/// In order to paint the image on screen, you first need to convert it to
|
||||
/// This is currently an enum with only one variant, but more image types may be added in the future.
|
||||
///
|
||||
/// See also: [`ColorImage`], [`FontImage`].
|
||||
#[derive(Clone, PartialEq)]
|
||||
/// See also: [`ColorImage`].
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum ImageData {
|
||||
/// RGBA image.
|
||||
Color(Arc<ColorImage>),
|
||||
|
||||
/// Used for the font texture.
|
||||
Font(FontImage),
|
||||
}
|
||||
|
||||
impl ImageData {
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
match self {
|
||||
Self::Color(image) => image.size,
|
||||
Self::Font(image) => image.size,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +34,7 @@ impl ImageData {
|
||||
|
||||
pub fn bytes_per_pixel(&self) -> usize {
|
||||
match self {
|
||||
Self::Color(_) | Self::Font(_) => 4,
|
||||
Self::Color(_) => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,6 +267,37 @@ impl ColorImage {
|
||||
}
|
||||
Self::new([width, height], output)
|
||||
}
|
||||
|
||||
/// Clone a sub-region as a new image.
|
||||
pub fn region_by_pixels(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> Self {
|
||||
assert!(
|
||||
x + w <= self.width(),
|
||||
"x + w should be <= self.width(), but x: {}, w: {}, width: {}",
|
||||
x,
|
||||
w,
|
||||
self.width()
|
||||
);
|
||||
assert!(
|
||||
y + h <= self.height(),
|
||||
"y + h should be <= self.height(), but y: {}, h: {}, height: {}",
|
||||
y,
|
||||
h,
|
||||
self.height()
|
||||
);
|
||||
|
||||
let mut pixels = Vec::with_capacity(w * h);
|
||||
for y in y..y + h {
|
||||
let offset = y * self.width() + x;
|
||||
pixels.extend(&self.pixels[offset..(offset + w)]);
|
||||
}
|
||||
assert_eq!(
|
||||
pixels.len(),
|
||||
w * h,
|
||||
"pixels.len should be w * h, but got {}",
|
||||
pixels.len()
|
||||
);
|
||||
Self::new([w, h], pixels)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<(usize, usize)> for ColorImage {
|
||||
@@ -318,127 +345,56 @@ impl std::fmt::Debug for ColorImage {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A single-channel image designed for the font texture.
|
||||
///
|
||||
/// Each value represents "coverage", i.e. how much a texel is covered by a character.
|
||||
///
|
||||
/// This is roughly interpreted as the opacity of a white image.
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
/// How to convert font coverage values into alpha and color values.
|
||||
//
|
||||
// This whole thing is less than rigorous.
|
||||
// Ideally we should do this in a shader instead, and use different computations
|
||||
// for different text colors.
|
||||
// See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct FontImage {
|
||||
/// width, height
|
||||
pub size: [usize; 2],
|
||||
|
||||
/// The coverage value.
|
||||
pub enum AlphaFromCoverage {
|
||||
/// `alpha = coverage`.
|
||||
///
|
||||
/// Often you want to use [`Self::srgba_pixels`] instead.
|
||||
pub pixels: Vec<f32>,
|
||||
}
|
||||
|
||||
impl FontImage {
|
||||
pub fn new(size: [usize; 2]) -> Self {
|
||||
Self {
|
||||
size,
|
||||
pixels: vec![0.0; size[0] * size[1]],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn width(&self) -> usize {
|
||||
self.size[0]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn height(&self) -> usize {
|
||||
self.size[1]
|
||||
}
|
||||
|
||||
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
|
||||
/// Looks good for black-on-white text, i.e. light mode.
|
||||
///
|
||||
/// `gamma` should normally be set to `None`.
|
||||
/// Same as [`Self::Gamma`]`(1.0)`, but more efficient.
|
||||
Linear,
|
||||
|
||||
/// `alpha = coverage^gamma`.
|
||||
Gamma(f32),
|
||||
|
||||
/// `alpha = 2 * coverage - coverage^2`
|
||||
///
|
||||
/// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`.
|
||||
#[inline]
|
||||
pub fn srgba_pixels(&self, gamma: Option<f32>) -> impl ExactSizeIterator<Item = Color32> + '_ {
|
||||
// This whole function is less than rigorous.
|
||||
// Ideally we should do this in a shader instead, and use different computations
|
||||
// for different text colors.
|
||||
// See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis.
|
||||
self.pixels.iter().map(move |coverage| {
|
||||
let alpha = if let Some(gamma) = gamma {
|
||||
coverage.powf(gamma)
|
||||
} else {
|
||||
// alpha = coverage * coverage; // recommended by the article for WHITE text (using linear blending)
|
||||
|
||||
// The following is recommended by the article for BLACK text (using linear blending).
|
||||
// Very similar to a gamma of 0.5, but produces sharper text.
|
||||
// In practice it works well for all text colors (better than a gamma of 0.5, for instance).
|
||||
// See https://www.desmos.com/calculator/w0ndf5blmn for a visual comparison.
|
||||
2.0 * coverage - coverage * coverage
|
||||
};
|
||||
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
|
||||
})
|
||||
}
|
||||
|
||||
/// Clone a sub-region as a new image.
|
||||
pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> Self {
|
||||
assert!(
|
||||
x + w <= self.width(),
|
||||
"x + w should be <= self.width(), but x: {}, w: {}, width: {}",
|
||||
x,
|
||||
w,
|
||||
self.width()
|
||||
);
|
||||
assert!(
|
||||
y + h <= self.height(),
|
||||
"y + h should be <= self.height(), but y: {}, h: {}, height: {}",
|
||||
y,
|
||||
h,
|
||||
self.height()
|
||||
);
|
||||
|
||||
let mut pixels = Vec::with_capacity(w * h);
|
||||
for y in y..y + h {
|
||||
let offset = y * self.width() + x;
|
||||
pixels.extend(&self.pixels[offset..(offset + w)]);
|
||||
}
|
||||
assert_eq!(
|
||||
pixels.len(),
|
||||
w * h,
|
||||
"pixels.len should be w * h, but got {}",
|
||||
pixels.len()
|
||||
);
|
||||
Self {
|
||||
size: [w, h],
|
||||
pixels,
|
||||
}
|
||||
}
|
||||
/// This looks good for white-on-black text, i.e. dark mode.
|
||||
///
|
||||
/// Very similar to a gamma of 0.5, but produces sharper text.
|
||||
/// See <https://www.desmos.com/calculator/w0ndf5blmn> for a comparison to gamma=0.5.
|
||||
#[default]
|
||||
TwoCoverageMinusCoverageSq,
|
||||
}
|
||||
|
||||
impl std::ops::Index<(usize, usize)> for FontImage {
|
||||
type Output = f32;
|
||||
impl AlphaFromCoverage {
|
||||
/// A good-looking default for light mode (black-on-white text).
|
||||
pub const LIGHT_MODE_DEFAULT: Self = Self::Linear;
|
||||
|
||||
#[inline]
|
||||
fn index(&self, (x, y): (usize, usize)) -> &f32 {
|
||||
let [w, h] = self.size;
|
||||
assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}");
|
||||
&self.pixels[y * w + x]
|
||||
}
|
||||
}
|
||||
/// A good-looking default for dark mode (white-on-black text).
|
||||
pub const DARK_MODE_DEFAULT: Self = Self::TwoCoverageMinusCoverageSq;
|
||||
|
||||
impl std::ops::IndexMut<(usize, usize)> for FontImage {
|
||||
#[inline]
|
||||
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut f32 {
|
||||
let [w, h] = self.size;
|
||||
assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}");
|
||||
&mut self.pixels[y * w + x]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FontImage> for ImageData {
|
||||
/// Convert coverage to alpha.
|
||||
#[inline(always)]
|
||||
fn from(image: FontImage) -> Self {
|
||||
Self::Font(image)
|
||||
pub fn alpha_from_coverage(&self, coverage: f32) -> f32 {
|
||||
match self {
|
||||
Self::Linear => coverage,
|
||||
Self::Gamma(gamma) => coverage.powf(*gamma),
|
||||
Self::TwoCoverageMinusCoverageSq => 2.0 * coverage - coverage * coverage,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn color_from_coverage(&self, coverage: f32) -> Color32 {
|
||||
let alpha = self.alpha_from_coverage(coverage);
|
||||
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,7 +403,7 @@ impl From<FontImage> for ImageData {
|
||||
/// A change to an image.
|
||||
///
|
||||
/// Either a whole new image, or an update to a rectangular region of it.
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[must_use = "The painter must take care of this"]
|
||||
pub struct ImageDelta {
|
||||
|
||||
@@ -50,7 +50,7 @@ pub use self::{
|
||||
color::ColorMode,
|
||||
corner_radius::CornerRadius,
|
||||
corner_radius_f32::CornerRadiusF32,
|
||||
image::{ColorImage, FontImage, ImageData, ImageDelta},
|
||||
image::{AlphaFromCoverage, ColorImage, ImageData, ImageDelta},
|
||||
margin::Margin,
|
||||
margin_f32::*,
|
||||
mesh::{Mesh, Mesh16, Vertex},
|
||||
|
||||
@@ -130,10 +130,12 @@ impl TextShape {
|
||||
num_vertices: _,
|
||||
num_indices: _,
|
||||
pixels_per_point: _,
|
||||
intrinsic_size,
|
||||
} = Arc::make_mut(galley);
|
||||
|
||||
*rect = transform.scaling * *rect;
|
||||
*mesh_bounds = transform.scaling * *mesh_bounds;
|
||||
*intrinsic_size = transform.scaling * *intrinsic_size;
|
||||
|
||||
for text::PlacedRow { pos, row } in rows {
|
||||
*pos *= transform.scaling;
|
||||
@@ -179,7 +181,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn text_bounding_box_under_rotation() {
|
||||
let fonts = Fonts::new(1.0, 1024, FontDefinitions::default());
|
||||
let fonts = Fonts::new(
|
||||
1.0,
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let font = FontId::monospace(12.0);
|
||||
|
||||
let mut t = crate::Shape::text(
|
||||
|
||||
@@ -279,12 +279,13 @@ impl FontImpl {
|
||||
} else {
|
||||
let glyph_pos = {
|
||||
let atlas = &mut self.atlas.lock();
|
||||
let text_alpha_from_coverage = atlas.text_alpha_from_coverage;
|
||||
let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height));
|
||||
glyph.draw(|x, y, v| {
|
||||
if 0.0 < v {
|
||||
let px = glyph_pos.0 + x as usize;
|
||||
let py = glyph_pos.1 + y as usize;
|
||||
image[(px, py)] = v;
|
||||
image[(px, py)] = text_alpha_from_coverage.color_from_coverage(v);
|
||||
}
|
||||
});
|
||||
glyph_pos
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
TextureAtlas,
|
||||
AlphaFromCoverage, TextureAtlas,
|
||||
mutex::{Mutex, MutexGuard},
|
||||
text::{
|
||||
Galley, LayoutJob, LayoutSection,
|
||||
@@ -430,36 +430,56 @@ impl Fonts {
|
||||
pub fn new(
|
||||
pixels_per_point: f32,
|
||||
max_texture_side: usize,
|
||||
text_alpha_from_coverage: AlphaFromCoverage,
|
||||
definitions: FontDefinitions,
|
||||
) -> Self {
|
||||
let fonts_and_cache = FontsAndCache {
|
||||
fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions),
|
||||
fonts: FontsImpl::new(
|
||||
pixels_per_point,
|
||||
max_texture_side,
|
||||
text_alpha_from_coverage,
|
||||
definitions,
|
||||
),
|
||||
galley_cache: Default::default(),
|
||||
};
|
||||
Self(Arc::new(Mutex::new(fonts_and_cache)))
|
||||
}
|
||||
|
||||
/// Call at the start of each frame with the latest known
|
||||
/// `pixels_per_point` and `max_texture_side`.
|
||||
/// `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`.
|
||||
///
|
||||
/// Call after painting the previous frame, but before using [`Fonts`] for the new frame.
|
||||
///
|
||||
/// This function will react to changes in `pixels_per_point` and `max_texture_side`,
|
||||
/// This function will react to changes in `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`,
|
||||
/// as well as notice when the font atlas is getting full, and handle that.
|
||||
pub fn begin_pass(&self, pixels_per_point: f32, max_texture_side: usize) {
|
||||
pub fn begin_pass(
|
||||
&self,
|
||||
pixels_per_point: f32,
|
||||
max_texture_side: usize,
|
||||
text_alpha_from_coverage: AlphaFromCoverage,
|
||||
) {
|
||||
let mut fonts_and_cache = self.0.lock();
|
||||
|
||||
let pixels_per_point_changed = fonts_and_cache.fonts.pixels_per_point != pixels_per_point;
|
||||
let max_texture_side_changed = fonts_and_cache.fonts.max_texture_side != max_texture_side;
|
||||
let text_alpha_from_coverage_changed =
|
||||
fonts_and_cache.fonts.atlas.lock().text_alpha_from_coverage != text_alpha_from_coverage;
|
||||
let font_atlas_almost_full = fonts_and_cache.fonts.atlas.lock().fill_ratio() > 0.8;
|
||||
let needs_recreate =
|
||||
pixels_per_point_changed || max_texture_side_changed || font_atlas_almost_full;
|
||||
let needs_recreate = pixels_per_point_changed
|
||||
|| max_texture_side_changed
|
||||
|| text_alpha_from_coverage_changed
|
||||
|| font_atlas_almost_full;
|
||||
|
||||
if needs_recreate {
|
||||
let definitions = fonts_and_cache.fonts.definitions.clone();
|
||||
|
||||
*fonts_and_cache = FontsAndCache {
|
||||
fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions),
|
||||
fonts: FontsImpl::new(
|
||||
pixels_per_point,
|
||||
max_texture_side,
|
||||
text_alpha_from_coverage,
|
||||
definitions,
|
||||
),
|
||||
galley_cache: Default::default(),
|
||||
};
|
||||
}
|
||||
@@ -497,7 +517,7 @@ impl Fonts {
|
||||
|
||||
/// The full font atlas image.
|
||||
#[inline]
|
||||
pub fn image(&self) -> crate::FontImage {
|
||||
pub fn image(&self) -> crate::ColorImage {
|
||||
self.lock().fonts.atlas.lock().image().clone()
|
||||
}
|
||||
|
||||
@@ -642,6 +662,7 @@ impl FontsImpl {
|
||||
pub fn new(
|
||||
pixels_per_point: f32,
|
||||
max_texture_side: usize,
|
||||
text_alpha_from_coverage: AlphaFromCoverage,
|
||||
definitions: FontDefinitions,
|
||||
) -> Self {
|
||||
assert!(
|
||||
@@ -651,7 +672,7 @@ impl FontsImpl {
|
||||
|
||||
let texture_width = max_texture_side.at_most(16 * 1024);
|
||||
let initial_height = 32; // Keep initial font atlas small, so it is fast to upload to GPU. This will expand as needed anyways.
|
||||
let atlas = TextureAtlas::new([texture_width, initial_height]);
|
||||
let atlas = TextureAtlas::new([texture_width, initial_height], text_alpha_from_coverage);
|
||||
|
||||
let atlas = Arc::new(Mutex::new(atlas));
|
||||
|
||||
@@ -1051,6 +1072,7 @@ mod tests {
|
||||
use core::f32;
|
||||
|
||||
use super::*;
|
||||
use crate::text::{TextWrapping, layout};
|
||||
use crate::{Stroke, text::TextFormat};
|
||||
use ecolor::Color32;
|
||||
use emath::Align;
|
||||
@@ -1120,6 +1142,7 @@ mod tests {
|
||||
let mut fonts = FontsImpl::new(
|
||||
pixels_per_point,
|
||||
max_texture_side,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
|
||||
@@ -1161,4 +1184,60 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intrinsic_size() {
|
||||
let pixels_per_point = [1.0, 1.3, 2.0, 0.867];
|
||||
let max_widths = [40.0, 80.0, 133.0, 200.0];
|
||||
let rounded_output_to_gui = [false, true];
|
||||
|
||||
for pixels_per_point in pixels_per_point {
|
||||
let mut fonts = FontsImpl::new(
|
||||
pixels_per_point,
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
|
||||
for &max_width in &max_widths {
|
||||
for round_output_to_gui in rounded_output_to_gui {
|
||||
for mut job in jobs() {
|
||||
job.wrap = TextWrapping::wrap_at_width(max_width);
|
||||
|
||||
job.round_output_to_gui = round_output_to_gui;
|
||||
|
||||
let galley_wrapped = layout(&mut fonts, job.clone().into());
|
||||
|
||||
job.wrap = TextWrapping::no_max_width();
|
||||
|
||||
let text = job.text.clone();
|
||||
let galley_unwrapped = layout(&mut fonts, job.into());
|
||||
|
||||
let intrinsic_size = galley_wrapped.intrinsic_size;
|
||||
let unwrapped_size = galley_unwrapped.size();
|
||||
|
||||
let difference = (intrinsic_size - unwrapped_size).length().abs();
|
||||
similar_asserts::assert_eq!(
|
||||
format!("{intrinsic_size:.4?}"),
|
||||
format!("{unwrapped_size:.4?}"),
|
||||
"Wrapped intrinsic size should almost match unwrapped size. Intrinsic: {intrinsic_size:.8?} vs unwrapped: {unwrapped_size:.8?}
|
||||
Difference: {difference:.8?}
|
||||
wrapped rows: {}, unwrapped rows: {}
|
||||
pixels_per_point: {pixels_per_point}, text: {text:?}, max_width: {max_width}, round_output_to_gui: {round_output_to_gui}",
|
||||
galley_wrapped.rows.len(),
|
||||
galley_unwrapped.rows.len()
|
||||
);
|
||||
similar_asserts::assert_eq!(
|
||||
format!("{intrinsic_size:.4?}"),
|
||||
format!("{unwrapped_size:.4?}"),
|
||||
"Unwrapped galley intrinsic size should exactly match its size. \
|
||||
{:.8?} vs {:8?}",
|
||||
galley_unwrapped.intrinsic_size,
|
||||
galley_unwrapped.size(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
|
||||
num_indices: 0,
|
||||
pixels_per_point: fonts.pixels_per_point(),
|
||||
elided: true,
|
||||
intrinsic_size: Vec2::ZERO,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,6 +95,8 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
|
||||
|
||||
let point_scale = PointScale::new(fonts.pixels_per_point());
|
||||
|
||||
let intrinsic_size = calculate_intrinsic_size(point_scale, &job, ¶graphs);
|
||||
|
||||
let mut elided = false;
|
||||
let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided);
|
||||
if elided {
|
||||
@@ -124,7 +127,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
|
||||
}
|
||||
|
||||
// Calculate the Y positions and tessellate the text:
|
||||
galley_from_rows(point_scale, job, rows, elided)
|
||||
galley_from_rows(point_scale, job, rows, elided, intrinsic_size)
|
||||
}
|
||||
|
||||
// Ignores the Y coordinate.
|
||||
@@ -190,6 +193,46 @@ fn layout_section(
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the intrinsic size of the text.
|
||||
///
|
||||
/// The result is eventually passed to `Response::intrinsic_size`.
|
||||
/// This works by calculating the size of each `Paragraph` (instead of each `Row`).
|
||||
fn calculate_intrinsic_size(
|
||||
point_scale: PointScale,
|
||||
job: &LayoutJob,
|
||||
paragraphs: &[Paragraph],
|
||||
) -> Vec2 {
|
||||
let mut intrinsic_size = Vec2::ZERO;
|
||||
for (idx, paragraph) in paragraphs.iter().enumerate() {
|
||||
if paragraph.glyphs.is_empty() {
|
||||
if idx == 0 {
|
||||
intrinsic_size.y += point_scale.round_to_pixel(paragraph.empty_paragraph_height);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
intrinsic_size.x = f32::max(
|
||||
paragraph
|
||||
.glyphs
|
||||
.last()
|
||||
.map(|l| l.max_x())
|
||||
.unwrap_or_default(),
|
||||
intrinsic_size.x,
|
||||
);
|
||||
|
||||
let mut height = paragraph
|
||||
.glyphs
|
||||
.iter()
|
||||
.map(|g| g.line_height)
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
|
||||
.unwrap_or(paragraph.empty_paragraph_height);
|
||||
if idx == 0 {
|
||||
height = f32::max(height, job.first_row_min_height);
|
||||
}
|
||||
intrinsic_size.y += point_scale.round_to_pixel(height);
|
||||
}
|
||||
intrinsic_size
|
||||
}
|
||||
|
||||
// Ignores the Y coordinate.
|
||||
fn rows_from_paragraphs(
|
||||
paragraphs: Vec<Paragraph>,
|
||||
@@ -610,6 +653,7 @@ fn galley_from_rows(
|
||||
job: Arc<LayoutJob>,
|
||||
mut rows: Vec<PlacedRow>,
|
||||
elided: bool,
|
||||
intrinsic_size: Vec2,
|
||||
) -> Galley {
|
||||
let mut first_row_min_height = job.first_row_min_height;
|
||||
let mut cursor_y = 0.0;
|
||||
@@ -680,6 +724,7 @@ fn galley_from_rows(
|
||||
num_vertices,
|
||||
num_indices,
|
||||
pixels_per_point: point_scale.pixels_per_point,
|
||||
intrinsic_size,
|
||||
};
|
||||
|
||||
if galley.job.round_output_to_gui {
|
||||
@@ -1034,11 +1079,18 @@ fn is_cjk_break_allowed(c: char) -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::AlphaFromCoverage;
|
||||
|
||||
use super::{super::*, *};
|
||||
|
||||
#[test]
|
||||
fn test_zero_max_width() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default());
|
||||
let mut fonts = FontsImpl::new(
|
||||
1.0,
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default());
|
||||
layout_job.wrap.max_width = 0.0;
|
||||
let galley = layout(&mut fonts, layout_job.into());
|
||||
@@ -1049,7 +1101,12 @@ mod tests {
|
||||
fn test_truncate_with_newline() {
|
||||
// No matter where we wrap, we should be appending the newline character.
|
||||
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default());
|
||||
let mut fonts = FontsImpl::new(
|
||||
1.0,
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let text_format = TextFormat {
|
||||
font_id: FontId::monospace(12.0),
|
||||
..Default::default()
|
||||
@@ -1094,7 +1151,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_cjk() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default());
|
||||
let mut fonts = FontsImpl::new(
|
||||
1.0,
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut layout_job = LayoutJob::single_section(
|
||||
"日本語とEnglishの混在した文章".into(),
|
||||
TextFormat::default(),
|
||||
@@ -1109,7 +1171,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_pre_cjk() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default());
|
||||
let mut fonts = FontsImpl::new(
|
||||
1.0,
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut layout_job = LayoutJob::single_section(
|
||||
"日本語とEnglishの混在した文章".into(),
|
||||
TextFormat::default(),
|
||||
@@ -1124,7 +1191,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_truncate_width() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default());
|
||||
let mut fonts = FontsImpl::new(
|
||||
1.0,
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut layout_job =
|
||||
LayoutJob::single_section("# DNA\nMore text".into(), TextFormat::default());
|
||||
layout_job.wrap.max_width = f32::INFINITY;
|
||||
|
||||
@@ -560,6 +560,12 @@ pub struct Galley {
|
||||
/// so that we can warn if this has changed once we get to
|
||||
/// tessellation.
|
||||
pub pixels_per_point: f32,
|
||||
|
||||
/// This is the size that a non-wrapped, non-truncated, non-justified version of the text
|
||||
/// would have.
|
||||
///
|
||||
/// Useful for advanced layouting.
|
||||
pub intrinsic_size: Vec2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -821,6 +827,8 @@ impl Galley {
|
||||
.at_most(rect.min.x + self.job.wrap.max_width)
|
||||
.floor_ui();
|
||||
}
|
||||
|
||||
self.intrinsic_size = self.intrinsic_size.round_ui();
|
||||
}
|
||||
|
||||
/// Append each galley under the previous one.
|
||||
@@ -836,6 +844,7 @@ impl Galley {
|
||||
num_vertices: 0,
|
||||
num_indices: 0,
|
||||
pixels_per_point,
|
||||
intrinsic_size: Vec2::ZERO,
|
||||
};
|
||||
|
||||
for (i, galley) in galleys.iter().enumerate() {
|
||||
@@ -872,6 +881,9 @@ impl Galley {
|
||||
// Note that if `galley.elided` is true this will be the last `Galley` in
|
||||
// the vector and the loop will end.
|
||||
merged_galley.elided |= galley.elided;
|
||||
merged_galley.intrinsic_size.x =
|
||||
f32::max(merged_galley.intrinsic_size.x, galley.intrinsic_size.x);
|
||||
merged_galley.intrinsic_size.y += galley.intrinsic_size.y;
|
||||
}
|
||||
|
||||
if merged_galley.job.round_output_to_gui {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ecolor::Color32;
|
||||
use emath::{Rect, remap_clamp};
|
||||
|
||||
use crate::{FontImage, ImageDelta};
|
||||
use crate::{AlphaFromCoverage, ColorImage, ImageDelta};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct Rectu {
|
||||
@@ -57,7 +58,7 @@ pub struct PreparedDisc {
|
||||
/// More characters can be added, possibly expanding the texture.
|
||||
#[derive(Clone)]
|
||||
pub struct TextureAtlas {
|
||||
image: FontImage,
|
||||
image: ColorImage,
|
||||
|
||||
/// What part of the image that is dirty
|
||||
dirty: Rectu,
|
||||
@@ -72,18 +73,22 @@ pub struct TextureAtlas {
|
||||
|
||||
/// pre-rasterized discs of radii `2^i`, where `i` is the index.
|
||||
discs: Vec<PrerasterizedDisc>,
|
||||
|
||||
/// Controls how to convert glyph coverage to alpha.
|
||||
pub(crate) text_alpha_from_coverage: AlphaFromCoverage,
|
||||
}
|
||||
|
||||
impl TextureAtlas {
|
||||
pub fn new(size: [usize; 2]) -> Self {
|
||||
pub fn new(size: [usize; 2], text_alpha_from_coverage: AlphaFromCoverage) -> Self {
|
||||
assert!(size[0] >= 1024, "Tiny texture atlas");
|
||||
let mut atlas = Self {
|
||||
image: FontImage::new(size),
|
||||
image: ColorImage::filled(size, Color32::TRANSPARENT),
|
||||
dirty: Rectu::EVERYTHING,
|
||||
cursor: (0, 0),
|
||||
row_height: 0,
|
||||
overflowed: false,
|
||||
discs: vec![], // will be filled in below
|
||||
text_alpha_from_coverage,
|
||||
};
|
||||
|
||||
// Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color:
|
||||
@@ -93,7 +98,7 @@ impl TextureAtlas {
|
||||
(0, 0),
|
||||
"Expected the first allocation to be at (0, 0), but was at {pos:?}"
|
||||
);
|
||||
image[pos] = 1.0;
|
||||
image[pos] = Color32::WHITE;
|
||||
|
||||
// Allocate a series of anti-aliased discs used to render small filled circles:
|
||||
// TODO(emilk): these circles can be packed A LOT better.
|
||||
@@ -116,7 +121,7 @@ impl TextureAtlas {
|
||||
let coverage =
|
||||
remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0);
|
||||
image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] =
|
||||
coverage;
|
||||
text_alpha_from_coverage.color_from_coverage(coverage);
|
||||
}
|
||||
}
|
||||
atlas.discs.push(PrerasterizedDisc {
|
||||
@@ -184,7 +189,7 @@ impl TextureAtlas {
|
||||
|
||||
/// The full font atlas image.
|
||||
#[inline]
|
||||
pub fn image(&self) -> &FontImage {
|
||||
pub fn image(&self) -> &ColorImage {
|
||||
&self.image
|
||||
}
|
||||
|
||||
@@ -200,14 +205,14 @@ impl TextureAtlas {
|
||||
} else {
|
||||
let pos = [dirty.min_x, dirty.min_y];
|
||||
let size = [dirty.max_x - dirty.min_x, dirty.max_y - dirty.min_y];
|
||||
let region = self.image.region(pos, size);
|
||||
let region = self.image.region_by_pixels(pos, size);
|
||||
Some(ImageDelta::partial(pos, region, texture_options))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the coordinates of where the rect ended up,
|
||||
/// and invalidates the region.
|
||||
pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut FontImage) {
|
||||
pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut ColorImage) {
|
||||
/// On some low-precision GPUs (my old iPad) characters get muddled up
|
||||
/// if we don't add some empty pixels between the characters.
|
||||
/// On modern high-precision GPUs this is not needed.
|
||||
@@ -254,13 +259,15 @@ impl TextureAtlas {
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_to_min_height(image: &mut FontImage, required_height: usize) -> bool {
|
||||
fn resize_to_min_height(image: &mut ColorImage, required_height: usize) -> bool {
|
||||
while required_height >= image.height() {
|
||||
image.size[1] *= 2; // double the height
|
||||
}
|
||||
|
||||
if image.width() * image.height() > image.pixels.len() {
|
||||
image.pixels.resize(image.width() * image.height(), 0.0);
|
||||
image
|
||||
.pixels
|
||||
.resize(image.width() * image.height(), Color32::TRANSPARENT);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -271,7 +271,7 @@ pub enum TextureWrapMode {
|
||||
/// What has been allocated and freed during the last period.
|
||||
///
|
||||
/// These are commands given to the integration painter.
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[must_use = "The painter must take care of this"]
|
||||
pub struct TexturesDelta {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# This script searches for the last CI run with your branch name, downloads the test_results artefact
|
||||
# This script searches for the last CI run with your branch name, downloads the test_results artifact
|
||||
# and replaces your existing snapshots with the new ones.
|
||||
# Make sure you have the gh cli installed and authenticated before running this script.
|
||||
# If prompted to select a default repo, choose the emilk/egui one
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ad14068e60fa678ee749925dd3713ee2b12a83ec1bca9c413bdeb9bc27d8ac20
|
||||
size 407795
|
||||
oid sha256:d59882afca42e766dddc36450a3331ca247a130e3796f99d0335ac370a7c3610
|
||||
size 425517
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user