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

Merge branch 'master' of https://github.com/emilk/egui into multiples_viewports

This commit is contained in:
Konkitoman
2023-09-30 09:39:45 +03:00
86 changed files with 2288 additions and 1413 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "egui"
version = "0.22.0"
version = "0.23.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "An easy-to-use immediate mode GUI that runs on both web and native"
edition = "2021"
@@ -22,9 +22,18 @@ all-features = true
[features]
default = ["default_fonts"]
## Exposes detailed accessibility implementation required by platform
## accessibility APIs. Also requires support in the egui integration.
accesskit = ["dep:accesskit"]
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`epaint::Vertex`], [`emath::Vec2`] etc to `&[u8]`.
bytemuck = ["epaint/bytemuck"]
## Show a debug-ui on hover including the stacktrace to the hovered item.
## This is very useful in finding the code that creates a part of the UI.
## Does not work on web.
callstack = ["dep:backtrace"]
## [`cint`](https://docs.rs/cint) enables interoperability with other color libraries.
cint = ["epaint/cint"]
@@ -67,7 +76,7 @@ unity = ["epaint/unity"]
[dependencies]
epaint = { version = "0.22.0", path = "../epaint", default-features = false }
epaint = { version = "0.23.0", path = "../epaint", default-features = false }
ahash = { version = "0.8.1", default-features = false, features = [
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
@@ -76,10 +85,10 @@ ahash = { version = "0.8.1", default-features = false, features = [
nohash-hasher = "0.2"
#! ### Optional dependencies
## Exposes detailed accessibility implementation required by platform
## accessibility APIs. Also requires support in the egui integration.
accesskit = { version = "0.11", optional = true }
backtrace = { version = "0.3", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }

View File

@@ -0,0 +1,186 @@
#[derive(Clone)]
struct Frame {
/// `_main` is usually as the deepest depth.
depth: usize,
name: String,
file_and_line: String,
}
/// Capture a callstack, skipping the frames that are not interesting.
///
/// In particular: slips everything before `egui::Context::run`,
/// and skipping all frames in the `egui::` namespace.
pub fn capture() -> String {
let mut frames = vec![];
let mut depth = 0;
backtrace::trace(|frame| {
// Resolve this instruction pointer to a symbol name
backtrace::resolve_frame(frame, |symbol| {
let mut file_and_line = symbol.filename().map(shorten_source_file_path);
if let Some(file_and_line) = &mut file_and_line {
if let Some(line_nr) = symbol.lineno() {
file_and_line.push_str(&format!(":{line_nr}"));
}
}
let file_and_line = file_and_line.unwrap_or_default();
let name = symbol
.name()
.map(|name| name.to_string())
.unwrap_or_default();
frames.push(Frame {
depth,
name,
file_and_line,
});
});
depth += 1; // note: we can resolve multiple symbols on the same frame.
true // keep going to the next frame
});
if frames.is_empty() {
return Default::default();
}
// Inclusive:
let mut min_depth = 0;
let mut max_depth = frames.len() - 1;
for frame in &frames {
if frame.name.starts_with("egui::callstack::capture") {
min_depth = frame.depth + 1;
}
if frame.name.starts_with("egui::context::Context::run") {
max_depth = frame.depth;
}
}
// Remove frames that are uninteresting:
frames.retain(|frame| {
// Keep some special frames to give the user a sense of chronology:
if frame.name == "main"
|| frame.name == "_main"
|| frame.name.starts_with("egui::context::Context::run")
|| frame.name.starts_with("eframe::run_native")
{
return true;
}
if frame.depth < min_depth || max_depth < frame.depth {
return false;
}
// Remove stuff that isn't user calls:
let skip_prefixes = [
// "backtrace::", // not needed, since we cut at at egui::callstack::capture
"egui::",
"<egui::",
"<F as egui::widgets::Widget>",
"egui_plot::",
"egui_extras::",
"core::ptr::drop_in_place<egui::ui::Ui>::",
"eframe::",
"core::ops::function::FnOnce::call_once",
"<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once",
];
for prefix in skip_prefixes {
if frame.name.starts_with(prefix) {
return false;
}
}
true
});
frames.reverse(); // main on top, i.e. chronological order. Same as Python.
let mut deepest_depth = 0;
let mut widest_file_line = 0;
for frame in &frames {
deepest_depth = frame.depth.max(deepest_depth);
widest_file_line = frame.file_and_line.len().max(widest_file_line);
}
let widest_depth = deepest_depth.to_string().len();
let mut formatted = String::new();
if !frames.is_empty() {
let mut last_depth = frames[0].depth;
for frame in &frames {
let Frame {
depth,
name,
file_and_line,
} = frame;
if frame.depth + 1 < last_depth || last_depth + 1 < frame.depth {
// Show that some frames were elided
formatted.push_str(&format!("{:widest_depth$}\n", ""));
}
formatted.push_str(&format!(
"{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}\n"
));
last_depth = frame.depth;
}
}
formatted
}
/// Shorten a path to a Rust source file from a callstack.
///
/// Example input:
/// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs`
/// * `crates/rerun/src/main.rs`
/// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs`
fn shorten_source_file_path(path: &std::path::Path) -> String {
// Look for `src` and strip everything up to it.
let components: Vec<_> = path.iter().map(|path| path.to_string_lossy()).collect();
let mut src_idx = None;
for (i, c) in components.iter().enumerate() {
if c == "src" {
src_idx = Some(i);
}
}
// Look for the last `src`:
if let Some(src_idx) = src_idx {
// Before `src` comes the name of the crate - let's include that:
let first_index = src_idx.saturating_sub(1);
let mut output = components[first_index].to_string();
for component in &components[first_index + 1..] {
output.push('/');
output.push_str(component);
}
output
} else {
// No `src` directory found - weird!
path.display().to_string()
}
}
#[test]
fn test_shorten_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
use std::str::FromStr as _;
let before = std::path::PathBuf::from_str(before).unwrap();
assert_eq!(shorten_source_file_path(&before), after);
}
}

View File

@@ -65,12 +65,12 @@ pub struct Area {
interactable: bool,
enabled: bool,
constrain: bool,
constrain_rect: Option<Rect>,
order: Order,
default_pos: Option<Pos2>,
pivot: Align2,
anchor: Option<(Align2, Vec2)>,
new_pos: Option<Pos2>,
drag_bounds: Option<Rect>,
}
impl Area {
@@ -80,13 +80,13 @@ impl Area {
movable: true,
interactable: true,
constrain: false,
constrain_rect: None,
enabled: true,
order: Order::Middle,
default_pos: None,
new_pos: None,
pivot: Align2::LEFT_TOP,
anchor: None,
drag_bounds: None,
}
}
@@ -155,6 +155,21 @@ impl Area {
self
}
/// Constraint the movement of the window to the given rectangle.
///
/// For instance: `.constrain_to(ctx.screen_rect())`.
pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
self.constrain = true;
self.constrain_rect = Some(constrain_rect);
self
}
#[deprecated = "Use `constrain_to` instead"]
pub fn drag_bounds(mut self, constrain_rect: Rect) -> Self {
self.constrain_rect = Some(constrain_rect);
self
}
/// Where the "root" of the area is.
///
/// For instance, if you set this to [`Align2::RIGHT_TOP`]
@@ -189,12 +204,6 @@ impl Area {
self.movable(false)
}
/// Constrain the area up to which the window can be dragged.
pub fn drag_bounds(mut self, bounds: Rect) -> Self {
self.drag_bounds = Some(bounds);
self
}
pub(crate) fn get_pivot(&self) -> Align2 {
if let Some((pivot, _)) = self.anchor {
pivot
@@ -209,7 +218,8 @@ pub(crate) struct Prepared {
state: State,
move_response: Response,
enabled: bool,
drag_bounds: Option<Rect>,
constrain: bool,
constrain_rect: Option<Rect>,
/// We always make windows invisible the first frame to hide "first-frame-jitters".
///
@@ -243,8 +253,8 @@ impl Area {
new_pos,
pivot,
anchor,
drag_bounds,
constrain,
constrain_rect,
} = self;
let layer_id = LayerId::new(order, id);
@@ -271,7 +281,7 @@ impl Area {
}
// interact right away to prevent frame-delay
let move_response = {
let mut move_response = {
let interact_id = layer_id.id.with("move");
let sense = if movable {
Sense::click_and_drag()
@@ -291,16 +301,8 @@ impl Area {
enabled,
);
// Important check - don't try to move e.g. a combobox popup!
if movable {
if move_response.dragged() {
state.pivot_pos += ctx.input(|i| i.pointer.delta());
}
state.set_left_top_pos(
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
.min,
);
if movable && move_response.dragged() {
state.pivot_pos += ctx.input(|i| i.pointer.delta());
}
if (move_response.dragged() || move_response.clicked())
@@ -314,21 +316,25 @@ impl Area {
move_response
};
state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos()));
if constrain {
state.set_left_top_pos(
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
.left_top(),
ctx.constrain_window_rect_to_area(state.rect(), constrain_rect)
.min,
);
}
state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos()));
// Update responsbe with posisbly moved/constrained rect:
move_response = move_response.with_new_rect(state.rect());
Prepared {
layer_id,
state,
move_response,
enabled,
drag_bounds,
constrain,
constrain_rect,
temporarily_invisible: is_new,
}
}
@@ -371,15 +377,19 @@ impl Prepared {
&mut self.state
}
pub(crate) fn drag_bounds(&self) -> Option<Rect> {
self.drag_bounds
pub(crate) fn constrain(&self) -> bool {
self.constrain
}
pub(crate) fn constrain_rect(&self) -> Option<Rect> {
self.constrain_rect
}
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
let screen_rect = ctx.screen_rect();
let bounds = if let Some(bounds) = self.drag_bounds {
bounds.intersect(screen_rect) // protect against infinite bounds
let constrain_rect = if let Some(constrain_rect) = self.constrain_rect {
constrain_rect.intersect(screen_rect) // protect against infinite bounds
} else {
let central_area = ctx.available_rect();
@@ -393,7 +403,7 @@ impl Prepared {
let max_rect = Rect::from_min_max(
self.state.left_top_pos(),
bounds
constrain_rect
.max
.at_least(self.state.left_top_pos() + Vec2::splat(32.0)),
);
@@ -401,9 +411,9 @@ impl Prepared {
let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // hacky
let clip_rect_margin = ctx.style().visuals.clip_rect_margin.max(shadow_radius);
let clip_rect = Rect::from_min_max(self.state.left_top_pos(), bounds.max)
let clip_rect = Rect::from_min_max(self.state.left_top_pos(), constrain_rect.max)
.expand(clip_rect_margin)
.intersect(bounds);
.intersect(constrain_rect);
let mut ui = Ui::new(
ctx.clone(),
@@ -424,7 +434,8 @@ impl Prepared {
mut state,
move_response,
enabled: _,
drag_bounds: _,
constrain: _,
constrain_rect: _,
temporarily_invisible: _,
} = self;

View File

@@ -260,9 +260,8 @@ fn show_tooltip_area_dyn<'c, R>(
Area::new(area_id)
.order(Order::Tooltip)
.fixed_pos(window_pos)
.constrain(true)
.constrain_to(ctx.screen_rect())
.interactable(false)
.drag_bounds(ctx.screen_rect())
.show(ctx, |ui| {
Frame::popup(&ctx.style())
.show(ui, |ui| {

View File

@@ -314,6 +314,7 @@ impl Resize {
state.store(ui.ctx(), id);
#[cfg(debug_assertions)]
if ui.ctx().style().debug.show_resize {
ui.ctx().debug_painter().debug_rect(
Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size),

View File

@@ -43,7 +43,7 @@ impl<'open> Window<'open> {
/// If you need a changing title, you must call `window.id(…)` with a fixed id.
pub fn new(title: impl Into<WidgetText>) -> Self {
let title = title.into().fallback_text_style(TextStyle::Heading);
let area = Area::new(Id::new(title.text()));
let area = Area::new(Id::new(title.text())).constrain(true);
Self {
title,
open: None,
@@ -146,11 +146,31 @@ impl<'open> Window<'open> {
}
/// Constrains this window to the screen bounds.
///
/// To change the area to constrain to, use [`Self::constraint_to`].
///
/// Default: `true`.
pub fn constrain(mut self, constrain: bool) -> Self {
self.area = self.area.constrain(constrain);
self
}
/// Constraint the movement of the window to the given rectangle.
///
/// For instance: `.constrain_to(ctx.screen_rect())`.
pub fn constraint_to(mut self, constrain_rect: Rect) -> Self {
self.area = self.area.constrain_to(constrain_rect);
self
}
#[deprecated = "Use `constrain_to` instead"]
pub fn drag_bounds(mut self, constrain_rect: Rect) -> Self {
#![allow(deprecated)]
self.area = self.area.drag_bounds(constrain_rect);
self
}
/// Where the "root" of the window is.
///
/// For instance, if you set this to [`Align2::RIGHT_TOP`]
@@ -276,12 +296,6 @@ impl<'open> Window<'open> {
self.scroll = self.scroll.drag_to_scroll(drag_to_scroll);
self
}
/// Constrain the area up to which the window can be dragged.
pub fn drag_bounds(mut self, bounds: Rect) -> Self {
self.area = self.area.drag_bounds(bounds);
self
}
}
impl<'open> Window<'open> {
@@ -452,13 +466,6 @@ impl<'open> Window<'open> {
content_inner
};
{
let pos = ctx
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
.left_top();
area.state_mut().set_left_top_pos(pos);
}
let full_response = area.end(ctx, area_content_ui);
let inner_response = InnerResponse {
@@ -562,9 +569,11 @@ fn interact(
resize_id: Id,
) -> Option<WindowInteraction> {
let new_rect = move_and_resize_window(ctx, &window_interaction)?;
let new_rect = ctx.round_rect_to_pixels(new_rect);
let mut new_rect = ctx.round_rect_to_pixels(new_rect);
let new_rect = ctx.constrain_window_rect_to_area(new_rect, area.drag_bounds());
if area.constrain() {
new_rect = ctx.constrain_window_rect_to_area(new_rect, area.constrain_rect());
}
// TODO(emilk): add this to a Window state instead as a command "move here next frame"
area.state_mut().set_left_top_pos(new_rect.left_top());

View File

@@ -830,6 +830,7 @@ impl Context {
// This solves the problem of overlapping widgets.
// Whichever widget is added LAST (=on top) gets the input:
if interact_rect.is_positive() && sense.interactive() {
#[cfg(debug_assertions)]
if self.style().debug.show_interactive_widgets {
Self::layer_painter(self, LayerId::debug()).rect(
interact_rect,
@@ -838,6 +839,8 @@ impl Context {
Stroke::new(1.0, Color32::YELLOW.additive().linear_multiply(0.05)),
);
}
#[cfg(debug_assertions)]
let mut show_blocking_widget = None;
self.write(|ctx| {
@@ -858,6 +861,7 @@ impl Context {
// Another interactive widget is covering us at the pointer position,
// so we aren't hovered.
#[cfg(debug_assertions)]
if ctx.memory.options.style.debug.show_blocking_widget {
// Store the rects to use them outside the write() call to
// avoid deadlock
@@ -873,6 +877,7 @@ impl Context {
}
});
#[cfg(debug_assertions)]
if let Some((interact_rect, prev_rect)) = show_blocking_widget {
Self::layer_painter(self, LayerId::debug()).debug_rect(
interact_rect,
@@ -1892,15 +1897,15 @@ impl Context {
// ---------------------------------------------------------------------
/// Whether or not to debug widget layout on hover.
#[cfg(debug_assertions)]
pub fn debug_on_hover(&self) -> bool {
self.options(|opt| opt.style.debug.debug_on_hover)
}
/// Turn on/off whether or not to debug widget layout on hover.
#[cfg(debug_assertions)]
pub fn set_debug_on_hover(&self, debug_on_hover: bool) {
let mut style = self.options(|opt| (*opt.style).clone());
style.debug.debug_on_hover = debug_on_hover;
self.set_style(style);
self.style_mut(|style| style.debug.debug_on_hover = debug_on_hover);
}
}
@@ -1991,7 +1996,6 @@ impl Context {
/// Show the state of egui, including its input and output.
pub fn inspection_ui(&self, ui: &mut Ui) {
use crate::containers::*;
crate::trace!(ui);
ui.label(format!("Is using pointer: {}", self.is_using_pointer()))
.on_hover_text(

View File

@@ -479,6 +479,11 @@ impl Modifiers {
!self.is_none()
}
#[inline]
pub fn all(&self) -> bool {
self.alt && self.ctrl && self.shift && self.command
}
/// Is shift the only pressed button?
#[inline]
pub fn shift_only(&self) -> bool {

View File

@@ -54,6 +54,9 @@ pub(crate) struct FrameState {
/// Highlight these widgets the next frame. Write to this.
pub(crate) highlight_next_frame: IdSet,
#[cfg(debug_assertions)]
pub(crate) has_debug_viewed_this_frame: bool,
}
impl Default for FrameState {
@@ -70,6 +73,9 @@ impl Default for FrameState {
accesskit_state: None,
highlight_this_frame: Default::default(),
highlight_next_frame: Default::default(),
#[cfg(debug_assertions)]
has_debug_viewed_this_frame: false,
}
}
}
@@ -89,6 +95,9 @@ impl FrameState {
accesskit_state,
highlight_this_frame,
highlight_next_frame,
#[cfg(debug_assertions)]
has_debug_viewed_this_frame,
} = self;
used_ids.clear();
@@ -99,6 +108,11 @@ impl FrameState {
*scroll_delta = input.scroll_delta;
*scroll_target = [None, None];
#[cfg(debug_assertions)]
{
*has_debug_viewed_this_frame = false;
}
#[cfg(feature = "accesskit")]
{
*accesskit_state = None;

View File

@@ -187,24 +187,27 @@ impl GridLayout {
}
pub(crate) fn advance(&mut self, cursor: &mut Rect, _frame_rect: Rect, widget_rect: Rect) {
let debug_expand_width = self.style.debug.show_expand_width;
let debug_expand_height = self.style.debug.show_expand_height;
if debug_expand_width || debug_expand_height {
let rect = widget_rect;
let too_wide = rect.width() > self.prev_col_width(self.col);
let too_high = rect.height() > self.prev_row_height(self.row);
#[cfg(debug_assertions)]
{
let debug_expand_width = self.style.debug.show_expand_width;
let debug_expand_height = self.style.debug.show_expand_height;
if debug_expand_width || debug_expand_height {
let rect = widget_rect;
let too_wide = rect.width() > self.prev_col_width(self.col);
let too_high = rect.height() > self.prev_row_height(self.row);
if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
let painter = self.ctx.debug_painter();
painter.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE));
if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
let painter = self.ctx.debug_painter();
painter.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE));
let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
let paint_line_seg = |a, b| painter.line_segment([a, b], stroke);
let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
let paint_line_seg = |a, b| painter.line_segment([a, b], stroke);
if debug_expand_width && too_wide {
paint_line_seg(rect.left_top(), rect.left_bottom());
paint_line_seg(rect.left_center(), rect.right_center());
paint_line_seg(rect.right_top(), rect.right_bottom());
if debug_expand_width && too_wide {
paint_line_seg(rect.left_top(), rect.left_bottom());
paint_line_seg(rect.left_center(), rect.right_center());
paint_line_seg(rect.right_top(), rect.right_bottom());
}
}
}
}

View File

@@ -801,6 +801,7 @@ impl Layout {
/// ## Debug stuff
impl Layout {
/// Shows where the next widget is going to be placed
#[cfg(debug_assertions)]
pub(crate) fn paint_text_at_cursor(
&self,
painter: &crate::Painter,

View File

@@ -12,6 +12,10 @@
//! Then you add a [`Window`] or a [`SidePanel`] to get a [`Ui`], which is what you'll be using to add all the buttons and labels that you need.
//!
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
//!
//! # Using egui
//!
//! To see what is possible to build with egui you can check out the online demo at <https://www.egui.rs/#demo>.
@@ -163,7 +167,14 @@
//!
//! # Understanding immediate mode
//!
//! `egui` is an immediate mode GUI library. It is useful to fully grok what "immediate mode" implies.
//! `egui` is an immediate mode GUI library.
//!
//! Immediate mode has its roots in gaming, where everything on the screen is painted at the
//! display refresh rate, i.e. at 60+ frames per second.
//! In immediate mode GUIs, the entire interface is laid out and painted at the same high rate.
//! This makes immediate mode GUIs especially well suited for highly interactive applications.
//!
//! It is useful to fully grok what "immediate mode" implies.
//!
//! Here is an example to illustrate it:
//!
@@ -198,7 +209,7 @@
//! # });
//! ```
//!
//! Here egui will read `value` to display the slider, then look if the mouse is dragging the slider and if so change the `value`.
//! Here egui will read `value` (an `f32`) to display the slider, then look if the mouse is dragging the slider and if so change the `value`.
//! Note that `egui` does not store the slider value for you - it only displays the current value, and changes it
//! by how much the slider has been dragged in the previous few milliseconds.
//! This means it is responsibility of the egui user to store the state (`value`) so that it persists between frames.
@@ -318,10 +329,6 @@
//! }); // the temporary settings are reverted here
//! # });
//! ```
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(clippy::float_cmp)]
#![allow(clippy::manual_range_contains)]
@@ -354,6 +361,10 @@ pub mod viewport;
pub mod widget_text;
pub mod widgets;
#[cfg(feature = "callstack")]
#[cfg(debug_assertions)]
mod callstack;
#[cfg(feature = "accesskit")]
pub use accesskit;
@@ -439,7 +450,8 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
/// ui.image(egui::include_image!("../assets/ferris.png"));
/// ui.add(
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
/// .rounding(egui::Rounding::same(6.0))
/// .max_width(200.0)
/// .rounding(10.0),
/// );
///
/// let image_source: egui::ImageSource = egui::include_image!("../assets/ferris.png");
@@ -449,10 +461,10 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
#[macro_export]
macro_rules! include_image {
($path: literal) => {
$crate::ImageSource::Bytes(
::std::borrow::Cow::Borrowed(concat!("bytes://", $path)), // uri
$crate::load::Bytes::Static(include_bytes!($path)),
)
$crate::ImageSource::Bytes {
uri: ::std::borrow::Cow::Borrowed(concat!("bytes://", $path)),
bytes: $crate::load::Bytes::Static(include_bytes!($path)),
}
};
}
@@ -488,32 +500,6 @@ macro_rules! github_link_file {
// ----------------------------------------------------------------------------
/// Show debug info on hover when [`Context::set_debug_on_hover`] has been turned on.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// // Turn on tracing of widgets
/// ui.ctx().set_debug_on_hover(true);
///
/// /// Show [`std::file`], [`std::line`] and argument on hover
/// egui::trace!(ui, "MyWindow");
///
/// /// Show [`std::file`] and [`std::line`] on hover
/// egui::trace!(ui);
/// # });
/// ```
#[macro_export]
macro_rules! trace {
($ui: expr) => {{
$ui.trace_location(format!("{}:{}", file!(), line!()))
}};
($ui: expr, $label: expr) => {{
$ui.trace_location(format!("{} - {}:{}", $label, file!(), line!()))
}};
}
// ----------------------------------------------------------------------------
/// An assert that is only active when `egui` is compiled with the `extra_asserts` feature
/// or with the `extra_debug_asserts` feature in debug builds.
#[macro_export]

View File

@@ -4,7 +4,7 @@
//! will get you up and running quickly with its reasonable default implementations of the traits described below.
//!
//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature.
//! 2. Add a call to [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html)
//! 2. Add a call to [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html)
//! in your app's setup code.
//! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`].
//!
@@ -55,17 +55,20 @@
mod bytes_loader;
mod texture_loader;
use crate::Context;
use std::borrow::Cow;
use std::fmt::Debug;
use std::ops::Deref;
use std::{error::Error as StdError, fmt::Display, sync::Arc};
use ahash::HashMap;
use epaint::mutex::Mutex;
use epaint::util::FloatOrd;
use epaint::util::OrderedFloat;
use epaint::TextureHandle;
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
use std::borrow::Cow;
use std::fmt::Debug;
use std::ops::Deref;
use std::{error::Error as StdError, fmt::Display, sync::Arc};
use crate::Context;
pub use self::bytes_loader::DefaultBytesLoader;
pub use self::texture_loader::DefaultTextureLoader;
@@ -76,15 +79,15 @@ pub enum LoadError {
/// Programmer error: There are no image loaders installed.
NoImageLoaders,
/// A specific loader does not support this schema, protocol or image format.
/// A specific loader does not support this scheme, protocol or image format.
NotSupported,
/// Programmer error: Failed to find the bytes for this image because
/// there was no [`BytesLoader`] supporting the schema.
/// there was no [`BytesLoader`] supporting the scheme.
NoMatchingBytesLoader,
/// Programmer error: Failed to parse the bytes as an image because
/// there was no [`ImageLoader`] supporting the schema.
/// there was no [`ImageLoader`] supporting the scheme.
NoMatchingImageLoader,
/// Programmer error: no matching [`TextureLoader`].
@@ -108,7 +111,7 @@ impl Display for LoadError {
Self::NoMatchingTextureLoader => f.write_str("No matching TextureLoader. Did you remove the default one?"),
Self::NotSupported => f.write_str("Iagge schema or URI not supported by this loader"),
Self::NotSupported => f.write_str("Image scheme or URI not supported by this loader"),
Self::Loading(message) => f.write_str(message),
}

View File

@@ -146,10 +146,9 @@ pub(crate) fn menu_ui<'c, R>(
let area = Area::new(menu_id)
.order(Order::Foreground)
.constrain(true)
.fixed_pos(pos)
.interactable(true)
.drag_bounds(ctx.screen_rect());
.constrain_to(ctx.screen_rect())
.interactable(true);
area.show(ctx, |ui| {
set_menu_style(ui.style_mut());
@@ -348,8 +347,7 @@ impl MenuRoot {
if let Some(root) = root.inner.as_mut() {
if root.id == id {
// pressed somewhere while this menu is open
let menu_state = root.menu_state.read();
let in_menu = menu_state.area_contains(pos);
let in_menu = root.menu_state.read().area_contains(pos);
if !in_menu {
return MenuResponse::Close;
}
@@ -374,8 +372,7 @@ impl MenuRoot {
let mut destroy = false;
let mut in_old_menu = false;
if let Some(root) = root {
let menu_state = root.menu_state.read();
in_old_menu = menu_state.area_contains(pos);
in_old_menu = root.menu_state.read().area_contains(pos);
destroy = root.id == response.id;
}
if !in_old_menu {

View File

@@ -263,6 +263,7 @@ impl Placer {
}
impl Placer {
#[cfg(debug_assertions)]
pub(crate) fn debug_paint_cursor(&self, painter: &crate::Painter, text: impl ToString) {
let stroke = Stroke::new(1.0, Color32::DEBUG_COLOR);

View File

@@ -201,6 +201,9 @@ pub struct Style {
pub animation_time: f32,
/// Options to help debug why egui behaves strangely.
///
/// Only available in debug builds.
#[cfg(debug_assertions)]
pub debug: DebugOptions,
/// Show tooltips explaining [`DragValue`]:s etc when hovered.
@@ -690,12 +693,36 @@ impl WidgetVisuals {
}
/// Options for help debug egui by adding extra visualization
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg(debug_assertions)]
pub struct DebugOptions {
/// However over widgets to see their rectangles
/// Always show callstack to ui on hover.
///
/// Useful for figuring out where in the code some UI is being created.
///
/// Only works in debug builds.
/// Requires the `callstack` feature.
/// Does not work on web.
#[cfg(debug_assertions)]
pub debug_on_hover: bool,
/// Show callstack for the current widget on hover if all modifier keys are pressed down.
///
/// Useful for figuring out where in the code some UI is being created.
///
/// Only works in debug builds.
/// Requires the `callstack` feature.
/// Does not work on web.
///
/// Default is `true` in debug builds, on native, if the `callstack` feature is enabled.
#[cfg(debug_assertions)]
pub debug_on_hover_with_all_modifiers: bool,
/// If we show the hover ui, include where the next widget is placed.
#[cfg(debug_assertions)]
pub hover_shows_next: bool,
/// Show which widgets make their parent wider
pub show_expand_width: bool,
@@ -711,6 +738,23 @@ pub struct DebugOptions {
pub show_blocking_widget: bool,
}
#[cfg(debug_assertions)]
impl Default for DebugOptions {
fn default() -> Self {
Self {
debug_on_hover: false,
debug_on_hover_with_all_modifiers: cfg!(feature = "callstack")
&& !cfg!(target_arch = "wasm32"),
hover_shows_next: false,
show_expand_width: false,
show_expand_height: false,
show_resize: false,
show_interactive_widgets: false,
show_blocking_widget: false,
}
}
}
// ----------------------------------------------------------------------------
/// The default text styles of the default egui theme.
@@ -739,6 +783,7 @@ impl Default for Style {
interaction: Interaction::default(),
visuals: Visuals::default(),
animation_time: 1.0 / 12.0,
#[cfg(debug_assertions)]
debug: Default::default(),
explanation_tooltips: false,
}
@@ -993,6 +1038,7 @@ impl Style {
interaction,
visuals,
animation_time,
#[cfg(debug_assertions)]
debug,
explanation_tooltips,
} = self;
@@ -1055,6 +1101,8 @@ impl Style {
ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
ui.collapsing("☝ Interaction", |ui| interaction.ui(ui));
ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
#[cfg(debug_assertions)]
ui.collapsing("🐛 Debug", |ui| debug.ui(ui));
ui.checkbox(explanation_tooltips, "Explanation tooltips")
@@ -1477,10 +1525,13 @@ impl Visuals {
}
}
#[cfg(debug_assertions)]
impl DebugOptions {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
debug_on_hover,
debug_on_hover_with_all_modifiers,
hover_shows_next,
show_expand_width,
show_expand_height,
show_resize,
@@ -1488,7 +1539,16 @@ impl DebugOptions {
show_blocking_widget,
} = self;
ui.checkbox(debug_on_hover, "Show debug info on hover");
{
ui.checkbox(debug_on_hover, "Show widget info on hover.");
ui.checkbox(
debug_on_hover_with_all_modifiers,
"Show widget info on hover if holding all modifier keys",
);
ui.checkbox(hover_shows_next, "Show next widget placement on hover");
}
ui.checkbox(
show_expand_width,
"Show which widgets make their parent wider",

View File

@@ -738,43 +738,41 @@ impl Ui {
/// # });
/// ```
pub fn allocate_space(&mut self, desired_size: Vec2) -> (Id, Rect) {
// For debug rendering
#[cfg(debug_assertions)]
let original_available = self.available_size_before_wrap();
let too_wide = desired_size.x > original_available.x;
let too_high = desired_size.y > original_available.y;
let rect = self.allocate_space_impl(desired_size);
if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) {
let painter = self.ctx().debug_painter();
painter.rect_stroke(rect, 4.0, (1.0, Color32::LIGHT_BLUE));
self.placer.debug_paint_cursor(&painter, "next");
}
#[cfg(debug_assertions)]
{
let too_wide = desired_size.x > original_available.x;
let too_high = desired_size.y > original_available.y;
let debug_expand_width = self.style().debug.show_expand_width;
let debug_expand_height = self.style().debug.show_expand_height;
let debug_expand_width = self.style().debug.show_expand_width;
let debug_expand_height = self.style().debug.show_expand_height;
if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
self.painter
.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE));
if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
self.painter
.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE));
let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
let paint_line_seg = |a, b| self.painter().line_segment([a, b], stroke);
let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
let paint_line_seg = |a, b| self.painter().line_segment([a, b], stroke);
if debug_expand_width && too_wide {
paint_line_seg(rect.left_top(), rect.left_bottom());
paint_line_seg(rect.left_center(), rect.right_center());
paint_line_seg(
pos2(rect.left() + original_available.x, rect.top()),
pos2(rect.left() + original_available.x, rect.bottom()),
);
paint_line_seg(rect.right_top(), rect.right_bottom());
}
if debug_expand_width && too_wide {
paint_line_seg(rect.left_top(), rect.left_bottom());
paint_line_seg(rect.left_center(), rect.right_center());
paint_line_seg(
pos2(rect.left() + original_available.x, rect.top()),
pos2(rect.left() + original_available.x, rect.bottom()),
);
paint_line_seg(rect.right_top(), rect.right_bottom());
}
if debug_expand_height && too_high {
paint_line_seg(rect.left_top(), rect.right_top());
paint_line_seg(rect.center_top(), rect.center_bottom());
paint_line_seg(rect.left_bottom(), rect.right_bottom());
if debug_expand_height && too_high {
paint_line_seg(rect.left_top(), rect.right_top());
paint_line_seg(rect.center_top(), rect.center_bottom());
paint_line_seg(rect.left_bottom(), rect.right_bottom());
}
}
}
@@ -795,6 +793,8 @@ impl Ui {
self.placer
.advance_after_rects(frame_rect, widget_rect, item_spacing);
register_rect(self, widget_rect);
widget_rect
}
@@ -803,6 +803,7 @@ impl Ui {
/// Ignore the layout of the [`Ui`]: just put my widget here!
/// The layout cursor will advance to past this `rect`.
pub fn allocate_rect(&mut self, rect: Rect, sense: Sense) -> Response {
register_rect(self, rect);
let id = self.advance_cursor_after_rect(rect);
self.interact(rect, id, sense)
}
@@ -813,12 +814,6 @@ impl Ui {
let item_spacing = self.spacing().item_spacing;
self.placer.advance_after_rects(rect, rect, item_spacing);
if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) {
let painter = self.ctx().debug_painter();
painter.rect_stroke(rect, 4.0, (1.0, Color32::LIGHT_BLUE));
self.placer.debug_paint_cursor(&painter, "next");
}
let id = Id::new(self.next_auto_id_source);
self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1);
id
@@ -896,13 +891,6 @@ impl Ui {
self.placer
.advance_after_rects(final_child_rect, final_child_rect, item_spacing);
if self.style().debug.debug_on_hover && self.rect_contains_pointer(final_child_rect) {
let painter = self.ctx().debug_painter();
painter.rect_stroke(frame_rect, 4.0, (1.0, Color32::LIGHT_BLUE));
painter.rect_stroke(final_child_rect, 4.0, (1.0, Color32::LIGHT_BLUE));
self.placer.debug_paint_cursor(&painter, "next");
}
let response = self.interact(final_child_rect, child_ui.id, Sense::hover());
InnerResponse::new(ret, response)
}
@@ -1561,7 +1549,7 @@ impl Ui {
/// Show an image available at the given `uri`.
///
/// ⚠ This will do nothing unless you install some image loaders first!
/// The easiest way to do this is via [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html).
/// The easiest way to do this is via [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html).
///
/// The loaders handle caching image data, sampled textures, etc. across frames, so calling this is immediate-mode safe.
///
@@ -1572,7 +1560,8 @@ impl Ui {
/// ui.image(egui::include_image!("../assets/ferris.png"));
/// ui.add(
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
/// .rounding(egui::Rounding::same(6.0))
/// .max_width(200.0)
/// .rounding(10.0),
/// );
/// # });
/// ```
@@ -1793,10 +1782,7 @@ impl Ui {
let mut child_rect = self.placer.available_rect_before_wrap();
child_rect.min.x += indent;
let mut child_ui = Self {
id: self.id.with(id_source),
..self.child_ui(child_rect, *self.layout())
};
let mut child_ui = self.child_ui_with_id_source(child_rect, *self.layout(), id_source);
let ret = add_contents(&mut child_ui);
let left_vline = self.visuals().indent_has_left_vline;
@@ -2024,12 +2010,6 @@ impl Ui {
let item_spacing = self.spacing().item_spacing;
self.placer.advance_after_rects(rect, rect, item_spacing);
if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) {
let painter = self.ctx().debug_painter();
painter.rect_stroke(rect, 4.0, (1.0, Color32::LIGHT_BLUE));
self.placer.debug_paint_cursor(&painter, "next");
}
InnerResponse::new(inner, self.interact(rect, child_ui.id, Sense::hover()))
}
@@ -2215,21 +2195,121 @@ impl Ui {
/// # Debug stuff
impl Ui {
/// Shows where the next widget is going to be placed
#[cfg(debug_assertions)]
pub fn debug_paint_cursor(&self) {
self.placer.debug_paint_cursor(&self.painter, "next");
}
}
/// Shows the given text where the next widget is to be placed
/// if when [`Context::set_debug_on_hover`] has been turned on and the mouse is hovering the Ui.
pub fn trace_location(&self, text: impl ToString) {
let rect = self.max_rect();
if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) {
self.placer
.debug_paint_cursor(&self.ctx().debug_painter(), text);
#[cfg(debug_assertions)]
impl Drop for Ui {
fn drop(&mut self) {
register_rect(self, self.min_rect());
}
}
/// Show this rectangle to the user if certain debug options are set.
#[cfg(debug_assertions)]
fn register_rect(ui: &Ui, rect: Rect) {
let debug = ui.style().debug;
let show_callstacks = debug.debug_on_hover
|| debug.debug_on_hover_with_all_modifiers && ui.input(|i| i.modifiers.all());
if !show_callstacks {
return;
}
if ui.ctx().frame_state(|o| o.has_debug_viewed_this_frame) {
return;
}
if !ui.rect_contains_pointer(rect) {
return;
}
// We only show one debug rectangle, or things get confusing:
ui.ctx()
.frame_state_mut(|o| o.has_debug_viewed_this_frame = true);
// ----------------------------------------------
let is_clicking = ui.input(|i| i.pointer.could_any_button_be_click());
// Use the debug-painter to avoid clip rect,
// otherwise the content of the widget may cover what we paint here!
let painter = ui.ctx().debug_painter();
// Paint rectangle around widget:
{
let rect_fg_color = if is_clicking {
Color32::WHITE
} else {
Color32::LIGHT_BLUE
};
let rect_bg_color = Color32::BLUE.gamma_multiply(0.5);
painter.rect(rect, 0.0, rect_bg_color, (1.0, rect_fg_color));
}
// ----------------------------------------------
if debug.hover_shows_next {
ui.placer.debug_paint_cursor(&painter, "next");
}
// ----------------------------------------------
#[cfg(feature = "callstack")]
let callstack = crate::callstack::capture();
#[cfg(not(feature = "callstack"))]
let callstack = String::default();
if !callstack.is_empty() {
let font_id = FontId::monospace(12.0);
let text = format!("{callstack}\n\n(click to copy)");
let galley = painter.layout_no_wrap(text, font_id, Color32::WHITE);
// Position the text either under or above:
let screen_rect = ui.ctx().screen_rect();
let y = if galley.size().y <= rect.top() {
// Above
rect.top() - galley.size().y
} else {
// Below
rect.bottom()
};
let y = y
.at_most(screen_rect.bottom() - galley.size().y)
.at_least(0.0);
let x = rect
.left()
.at_most(screen_rect.right() - galley.size().x)
.at_least(0.0);
let text_pos = pos2(x, y);
let text_bg_color = Color32::from_black_alpha(180);
let text_rect_stroke_color = if is_clicking {
Color32::WHITE
} else {
text_bg_color
};
let text_rect = Rect::from_min_size(text_pos, galley.size());
painter.rect(text_rect, 0.0, text_bg_color, (1.0, text_rect_stroke_color));
painter.galley(text_pos, galley);
if ui.input(|i| i.pointer.any_click()) {
ui.ctx().copy_text(callstack);
}
}
}
#[cfg(not(debug_assertions))]
fn register_rect(_ui: &Ui, _rect: Rect) {}
#[test]
fn ui_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}

View File

@@ -588,10 +588,9 @@ impl PersistedMap {
crate::profile_scope!("gather");
for (hash, element) in &map.map {
if let Some(element) = element.to_serialize() {
let mut stats = types_map.entry(element.type_id).or_default();
let stats = types_map.entry(element.type_id).or_default();
stats.num_bytes += element.ron.len();
let mut generation_stats =
stats.generations.entry(element.generation).or_default();
let generation_stats = stats.generations.entry(element.generation).or_default();
generation_stats.num_bytes += element.ron.len();
generation_stats.elements.push((*hash, element));
} else {

View File

@@ -100,7 +100,10 @@ impl<'a> Image<'a> {
///
/// See [`ImageSource::Bytes`].
pub fn from_bytes(uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) -> Self {
Self::new(ImageSource::Bytes(uri.into(), bytes.into()))
Self::new(ImageSource::Bytes {
uri: uri.into(),
bytes: bytes.into(),
})
}
/// Texture options used when creating the texture.
@@ -275,7 +278,7 @@ impl<'a> Image<'a> {
pub fn size(&self) -> Option<Vec2> {
match &self.source {
ImageSource::Texture(texture) => Some(texture.size),
ImageSource::Uri(_) | ImageSource::Bytes(_, _) => None,
ImageSource::Uri(_) | ImageSource::Bytes { .. } => None,
}
}
@@ -478,9 +481,10 @@ impl Default for ImageSize {
/// This is used by [`Image::new`] and [`Ui::image`].
#[derive(Clone)]
pub enum ImageSource<'a> {
/// Load the image from a URI.
/// Load the image from a URI, e.g. `https://example.com/image.png`.
///
/// This could be a `file://` path, `https://` url, `bytes://` identifier, or some other scheme.
///
/// This could be a `file://` url, `http(s)?://` url, or a `bare` identifier.
/// How the URI will be turned into a texture for rendering purposes is
/// up to the registered loaders to handle.
///
@@ -495,8 +499,6 @@ pub enum ImageSource<'a> {
/// Load the image from some raw bytes.
///
/// For better error messages, use the `bytes://` prefix for the URI.
///
/// The [`Bytes`] may be:
/// - `'static`, obtained from `include_bytes!` or similar
/// - Anything that can be converted to `Arc<[u8]>`
@@ -506,13 +508,22 @@ pub enum ImageSource<'a> {
/// See also [`include_image`] for an easy way to load and display static images.
///
/// See [`crate::load`] for more information.
Bytes(Cow<'static, str>, Bytes),
Bytes {
/// The unique identifier for this image, e.g. `bytes://my_logo.png`.
///
/// You should use a proper extension (`.jpg`, `.png`, `.svg`, etc) for the image to load properly.
///
/// Use the `bytes://` scheme for the URI for better error messages.
uri: Cow<'static, str>,
bytes: Bytes,
},
}
impl<'a> std::fmt::Debug for ImageSource<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImageSource::Bytes(uri, _) | ImageSource::Uri(uri) => uri.as_ref().fmt(f),
ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => uri.as_ref().fmt(f),
ImageSource::Texture(st) => st.id.fmt(f),
}
}
@@ -524,7 +535,7 @@ impl<'a> ImageSource<'a> {
pub fn texture_size(&self) -> Option<Vec2> {
match self {
ImageSource::Texture(texture) => Some(texture.size),
ImageSource::Uri(_) | ImageSource::Bytes(_, _) => None,
ImageSource::Uri(_) | ImageSource::Bytes { .. } => None,
}
}
@@ -539,7 +550,7 @@ impl<'a> ImageSource<'a> {
match self {
Self::Texture(texture) => Ok(TexturePoll::Ready { texture }),
Self::Uri(uri) => ctx.try_load_texture(uri.as_ref(), texture_options, size_hint),
Self::Bytes(uri, bytes) => {
Self::Bytes { uri, bytes } => {
ctx.include_bytes(uri.clone(), bytes);
ctx.try_load_texture(uri.as_ref(), texture_options, size_hint)
}
@@ -551,7 +562,7 @@ impl<'a> ImageSource<'a> {
/// This will return `None` for [`Self::Texture`].
pub fn uri(&self) -> Option<&str> {
match self {
ImageSource::Bytes(uri, _) | ImageSource::Uri(uri) => Some(uri),
ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => Some(uri),
ImageSource::Texture(_) => None,
}
}
@@ -644,21 +655,30 @@ impl<'a> From<Cow<'a, str>> for ImageSource<'a> {
impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
#[inline]
fn from((uri, bytes): (&'static str, T)) -> Self {
Self::Bytes(uri.into(), bytes.into())
Self::Bytes {
uri: uri.into(),
bytes: bytes.into(),
}
}
}
impl<T: Into<Bytes>> From<(Cow<'static, str>, T)> for ImageSource<'static> {
#[inline]
fn from((uri, bytes): (Cow<'static, str>, T)) -> Self {
Self::Bytes(uri, bytes.into())
Self::Bytes {
uri,
bytes: bytes.into(),
}
}
}
impl<T: Into<Bytes>> From<(String, T)> for ImageSource<'static> {
#[inline]
fn from((uri, bytes): (String, T)) -> Self {
Self::Bytes(uri.into(), bytes.into())
Self::Bytes {
uri: uri.into(),
bytes: bytes.into(),
}
}
}