mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 14:20:04 -04:00
Merge branch 'master' of https://github.com/emilk/egui into multiples_viewports
This commit is contained in:
@@ -4,7 +4,7 @@ version = "0.22.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"
|
||||
rust-version = "1.65"
|
||||
rust-version = "1.67"
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "../../README.md"
|
||||
|
||||
@@ -426,7 +426,7 @@ impl Prepared {
|
||||
temporarily_invisible: _,
|
||||
} = self;
|
||||
|
||||
state.size = content_ui.min_rect().size();
|
||||
state.size = content_ui.min_size();
|
||||
|
||||
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ impl CollapsingState {
|
||||
ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
|
||||
}
|
||||
|
||||
pub fn remove(&self, ctx: &Context) {
|
||||
ctx.data_mut(|d| d.remove::<InnerState>(self.id));
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Id {
|
||||
self.id
|
||||
}
|
||||
|
||||
@@ -193,9 +193,7 @@ impl Frame {
|
||||
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||
let outer_rect_bounds = ui.available_rect_before_wrap();
|
||||
|
||||
let mut inner_rect = outer_rect_bounds;
|
||||
inner_rect.min += self.outer_margin.left_top() + self.inner_margin.left_top();
|
||||
inner_rect.max -= self.outer_margin.right_bottom() + self.inner_margin.right_bottom();
|
||||
let mut inner_rect = (self.inner_margin + self.outer_margin).shrink_rect(outer_rect_bounds);
|
||||
|
||||
// Make sure we don't shrink to the negative:
|
||||
inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x);
|
||||
@@ -256,17 +254,13 @@ impl Frame {
|
||||
|
||||
impl Prepared {
|
||||
fn paint_rect(&self) -> Rect {
|
||||
let mut rect = self.content_ui.min_rect();
|
||||
rect.min -= self.frame.inner_margin.left_top();
|
||||
rect.max += self.frame.inner_margin.right_bottom();
|
||||
rect
|
||||
self.frame
|
||||
.inner_margin
|
||||
.expand_rect(self.content_ui.min_rect())
|
||||
}
|
||||
|
||||
fn content_with_margin(&self) -> Rect {
|
||||
let mut rect = self.content_ui.min_rect();
|
||||
rect.min -= self.frame.inner_margin.left_top() + self.frame.outer_margin.left_top();
|
||||
rect.max += self.frame.inner_margin.right_bottom() + self.frame.outer_margin.right_bottom();
|
||||
rect
|
||||
(self.frame.inner_margin + self.frame.outer_margin).expand_rect(self.content_ui.min_rect())
|
||||
}
|
||||
|
||||
pub fn end(self, ui: &mut Ui) -> Response {
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
//!
|
||||
//! Add your [`Window`]:s after any top-level panels.
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// State regarding panels.
|
||||
@@ -99,7 +97,7 @@ pub struct SidePanel {
|
||||
resizable: bool,
|
||||
show_separator_line: bool,
|
||||
default_width: f32,
|
||||
width_range: RangeInclusive<f32>,
|
||||
width_range: Rangef,
|
||||
}
|
||||
|
||||
impl SidePanel {
|
||||
@@ -122,7 +120,7 @@ impl SidePanel {
|
||||
resizable: true,
|
||||
show_separator_line: true,
|
||||
default_width: 200.0,
|
||||
width_range: 96.0..=f32::INFINITY,
|
||||
width_range: Rangef::new(96.0, f32::INFINITY),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,26 +151,29 @@ impl SidePanel {
|
||||
/// The initial wrapping width of the [`SidePanel`].
|
||||
pub fn default_width(mut self, default_width: f32) -> Self {
|
||||
self.default_width = default_width;
|
||||
self.width_range = self.width_range.start().at_most(default_width)
|
||||
..=self.width_range.end().at_least(default_width);
|
||||
self.width_range = Rangef::new(
|
||||
self.width_range.min.at_most(default_width),
|
||||
self.width_range.max.at_least(default_width),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Minimum width of the panel.
|
||||
pub fn min_width(mut self, min_width: f32) -> Self {
|
||||
self.width_range = min_width..=self.width_range.end().at_least(min_width);
|
||||
self.width_range = Rangef::new(min_width, self.width_range.max.at_least(min_width));
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum width of the panel.
|
||||
pub fn max_width(mut self, max_width: f32) -> Self {
|
||||
self.width_range = self.width_range.start().at_most(max_width)..=max_width;
|
||||
self.width_range = Rangef::new(self.width_range.min.at_most(max_width), max_width);
|
||||
self
|
||||
}
|
||||
|
||||
/// The allowable width range for the panel.
|
||||
pub fn width_range(mut self, width_range: RangeInclusive<f32>) -> Self {
|
||||
self.default_width = clamp_to_range(self.default_width, width_range.clone());
|
||||
pub fn width_range(mut self, width_range: impl Into<Rangef>) -> Self {
|
||||
let width_range = width_range.into();
|
||||
self.default_width = clamp_to_range(self.default_width, width_range);
|
||||
self.width_range = width_range;
|
||||
self
|
||||
}
|
||||
@@ -180,7 +181,7 @@ impl SidePanel {
|
||||
/// Enforce this exact width.
|
||||
pub fn exact_width(mut self, width: f32) -> Self {
|
||||
self.default_width = width;
|
||||
self.width_range = width..=width;
|
||||
self.width_range = Rangef::point(width);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -224,7 +225,7 @@ impl SidePanel {
|
||||
if let Some(state) = PanelState::load(ui.ctx(), id) {
|
||||
width = state.rect.width();
|
||||
}
|
||||
width = clamp_to_range(width, width_range.clone()).at_most(available_rect.width());
|
||||
width = clamp_to_range(width, width_range).at_most(available_rect.width());
|
||||
side.set_rect_width(&mut panel_rect, width);
|
||||
ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel");
|
||||
}
|
||||
@@ -241,7 +242,7 @@ impl SidePanel {
|
||||
|
||||
let resize_x = side.opposite().side_x(panel_rect);
|
||||
let mouse_over_resize_line = we_are_on_top
|
||||
&& panel_rect.y_range().contains(&pointer.y)
|
||||
&& panel_rect.y_range().contains(pointer.y)
|
||||
&& (resize_x - pointer.x).abs()
|
||||
<= ui.style().interaction.resize_grab_radius_side;
|
||||
|
||||
@@ -253,8 +254,7 @@ impl SidePanel {
|
||||
is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id));
|
||||
if is_resizing {
|
||||
let width = (pointer.x - side.side_x(panel_rect)).abs();
|
||||
let width =
|
||||
clamp_to_range(width, width_range.clone()).at_most(available_rect.width());
|
||||
let width = clamp_to_range(width, width_range).at_most(available_rect.width());
|
||||
side.set_rect_width(&mut panel_rect, width);
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ impl SidePanel {
|
||||
let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
|
||||
let inner_response = frame.show(&mut panel_ui, |ui| {
|
||||
ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height
|
||||
ui.set_min_width(*width_range.start());
|
||||
ui.set_min_width(width_range.min);
|
||||
add_contents(ui)
|
||||
});
|
||||
|
||||
@@ -544,7 +544,7 @@ pub struct TopBottomPanel {
|
||||
resizable: bool,
|
||||
show_separator_line: bool,
|
||||
default_height: Option<f32>,
|
||||
height_range: RangeInclusive<f32>,
|
||||
height_range: Rangef,
|
||||
}
|
||||
|
||||
impl TopBottomPanel {
|
||||
@@ -567,7 +567,7 @@ impl TopBottomPanel {
|
||||
resizable: false,
|
||||
show_separator_line: true,
|
||||
default_height: None,
|
||||
height_range: 20.0..=f32::INFINITY,
|
||||
height_range: Rangef::new(20.0, f32::INFINITY),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,28 +599,31 @@ impl TopBottomPanel {
|
||||
/// Defaults to [`style::Spacing::interact_size`].y.
|
||||
pub fn default_height(mut self, default_height: f32) -> Self {
|
||||
self.default_height = Some(default_height);
|
||||
self.height_range = self.height_range.start().at_most(default_height)
|
||||
..=self.height_range.end().at_least(default_height);
|
||||
self.height_range = Rangef::new(
|
||||
self.height_range.min.at_most(default_height),
|
||||
self.height_range.max.at_least(default_height),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Minimum height of the panel.
|
||||
pub fn min_height(mut self, min_height: f32) -> Self {
|
||||
self.height_range = min_height..=self.height_range.end().at_least(min_height);
|
||||
self.height_range = Rangef::new(min_height, self.height_range.max.at_least(min_height));
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum height of the panel.
|
||||
pub fn max_height(mut self, max_height: f32) -> Self {
|
||||
self.height_range = self.height_range.start().at_most(max_height)..=max_height;
|
||||
self.height_range = Rangef::new(self.height_range.min.at_most(max_height), max_height);
|
||||
self
|
||||
}
|
||||
|
||||
/// The allowable height range for the panel.
|
||||
pub fn height_range(mut self, height_range: RangeInclusive<f32>) -> Self {
|
||||
pub fn height_range(mut self, height_range: impl Into<Rangef>) -> Self {
|
||||
let height_range = height_range.into();
|
||||
self.default_height = self
|
||||
.default_height
|
||||
.map(|default_height| clamp_to_range(default_height, height_range.clone()));
|
||||
.map(|default_height| clamp_to_range(default_height, height_range));
|
||||
self.height_range = height_range;
|
||||
self
|
||||
}
|
||||
@@ -628,7 +631,7 @@ impl TopBottomPanel {
|
||||
/// Enforce this exact height.
|
||||
pub fn exact_height(mut self, height: f32) -> Self {
|
||||
self.default_height = Some(height);
|
||||
self.height_range = height..=height;
|
||||
self.height_range = Rangef::point(height);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -673,7 +676,7 @@ impl TopBottomPanel {
|
||||
} else {
|
||||
default_height.unwrap_or_else(|| ui.style().spacing.interact_size.y)
|
||||
};
|
||||
height = clamp_to_range(height, height_range.clone()).at_most(available_rect.height());
|
||||
height = clamp_to_range(height, height_range).at_most(available_rect.height());
|
||||
side.set_rect_height(&mut panel_rect, height);
|
||||
ui.ctx()
|
||||
.check_for_id_clash(id, panel_rect, "TopBottomPanel");
|
||||
@@ -692,7 +695,7 @@ impl TopBottomPanel {
|
||||
|
||||
let resize_y = side.opposite().side_y(panel_rect);
|
||||
let mouse_over_resize_line = we_are_on_top
|
||||
&& panel_rect.x_range().contains(&pointer.x)
|
||||
&& panel_rect.x_range().contains(pointer.x)
|
||||
&& (resize_y - pointer.y).abs()
|
||||
<= ui.style().interaction.resize_grab_radius_side;
|
||||
|
||||
@@ -704,8 +707,8 @@ impl TopBottomPanel {
|
||||
is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id));
|
||||
if is_resizing {
|
||||
let height = (pointer.y - side.side_y(panel_rect)).abs();
|
||||
let height = clamp_to_range(height, height_range.clone())
|
||||
.at_most(available_rect.height());
|
||||
let height =
|
||||
clamp_to_range(height, height_range).at_most(available_rect.height());
|
||||
side.set_rect_height(&mut panel_rect, height);
|
||||
}
|
||||
|
||||
@@ -724,7 +727,7 @@ impl TopBottomPanel {
|
||||
let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
|
||||
let inner_response = frame.show(&mut panel_ui, |ui| {
|
||||
ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width
|
||||
ui.set_min_height(*height_range.start());
|
||||
ui.set_min_height(height_range.min);
|
||||
add_contents(ui)
|
||||
});
|
||||
|
||||
@@ -1056,9 +1059,7 @@ impl CentralPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_to_range(x: f32, range: RangeInclusive<f32>) -> f32 {
|
||||
x.clamp(
|
||||
range.start().min(*range.end()),
|
||||
range.start().max(*range.end()),
|
||||
)
|
||||
fn clamp_to_range(x: f32, range: Rangef) -> f32 {
|
||||
let range = range.as_positive();
|
||||
x.clamp(range.min, range.max)
|
||||
}
|
||||
|
||||
@@ -124,7 +124,10 @@ impl Resize {
|
||||
}
|
||||
|
||||
/// Can you resize it with the mouse?
|
||||
/// Note that a window can still auto-resize
|
||||
///
|
||||
/// Note that a window can still auto-resize.
|
||||
///
|
||||
/// Default is `true`.
|
||||
pub fn resizable(mut self, resizable: bool) -> Self {
|
||||
self.resizable = resizable;
|
||||
self
|
||||
|
||||
@@ -334,16 +334,22 @@ struct Prepared {
|
||||
state: State,
|
||||
has_bar: [bool; 2],
|
||||
auto_shrink: [bool; 2],
|
||||
|
||||
/// How much horizontal and vertical space are used up by the
|
||||
/// width of the vertical bar, and the height of the horizontal bar?
|
||||
current_bar_use: Vec2,
|
||||
|
||||
scroll_bar_visibility: ScrollBarVisibility,
|
||||
|
||||
/// Where on the screen the content is (excludes scroll bars).
|
||||
inner_rect: Rect,
|
||||
|
||||
content_ui: Ui,
|
||||
|
||||
/// Relative coordinates: the offset and size of the view of the inner UI.
|
||||
/// `viewport.min == ZERO` means we scrolled to the top.
|
||||
viewport: Rect,
|
||||
|
||||
scrolling_enabled: bool,
|
||||
stick_to_end: [bool; 2],
|
||||
}
|
||||
@@ -459,7 +465,7 @@ impl ScrollArea {
|
||||
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
||||
}
|
||||
}
|
||||
// Make sure we din't accidentally expand the clip rect
|
||||
// Make sure we didn't accidentally expand the clip rect
|
||||
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
|
||||
content_ui.set_clip_rect(content_clip_rect);
|
||||
}
|
||||
@@ -640,8 +646,7 @@ impl Prepared {
|
||||
let min = content_ui.min_rect().min[d];
|
||||
let clip_rect = content_ui.clip_rect();
|
||||
let visible_range = min..=min + clip_rect.size()[d];
|
||||
let start = *scroll.start();
|
||||
let end = *scroll.end();
|
||||
let (start, end) = (scroll.min, scroll.max);
|
||||
let clip_start = clip_rect.min[d];
|
||||
let clip_end = clip_rect.max[d];
|
||||
let mut spacing = ui.spacing().item_spacing[d];
|
||||
|
||||
@@ -232,7 +232,10 @@ impl<'open> Window<'open> {
|
||||
}
|
||||
|
||||
/// Can the user resize the window by dragging its edges?
|
||||
///
|
||||
/// Note that even if you set this to `false` the window may still auto-resize.
|
||||
///
|
||||
/// Default is `true`.
|
||||
pub fn resizable(mut self, resizable: bool) -> Self {
|
||||
self.resize = self.resize.resizable(resizable);
|
||||
self
|
||||
@@ -278,6 +281,14 @@ impl<'open> Window<'open> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable/disable scrolling on the window by dragging with the pointer. `true` by default.
|
||||
///
|
||||
/// See [`ScrollArea::drag_to_scroll`] for more.
|
||||
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
|
||||
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);
|
||||
@@ -1701,7 +1712,7 @@ impl TitleBar {
|
||||
// Don't cover the close- and collapse buttons:
|
||||
// After 32 is used for a temporary embedd button!
|
||||
let double_click_rect = self.rect.shrink2(vec2(
|
||||
32.0 + ui.style().visuals.text_cursor_width + ui.style().spacing.icon_width,
|
||||
32.0 + ui.style().visuals.text_cursor.width + ui.style().spacing.icon_width,
|
||||
0.0,
|
||||
));
|
||||
|
||||
|
||||
@@ -744,7 +744,7 @@ impl Context {
|
||||
}
|
||||
|
||||
let show_error = |widget_rect: Rect, text: String| {
|
||||
let text = format!("🔥 {}", text);
|
||||
let text = format!("🔥 {text}");
|
||||
let color = self.style().visuals.error_fg_color;
|
||||
let painter = self.debug_painter();
|
||||
painter.rect_stroke(widget_rect, 0.0, (1.0, color));
|
||||
@@ -790,10 +790,10 @@ impl Context {
|
||||
let id_str = id.short_debug_format();
|
||||
|
||||
if prev_rect.min.distance(new_rect.min) < 4.0 {
|
||||
show_error(new_rect, format!("Double use of {} ID {}", what, id_str));
|
||||
show_error(new_rect, format!("Double use of {what} ID {id_str}"));
|
||||
} else {
|
||||
show_error(prev_rect, format!("First use of {} ID {}", what, id_str));
|
||||
show_error(new_rect, format!("Second use of {} ID {}", what, id_str));
|
||||
show_error(prev_rect, format!("First use of {what} ID {id_str}"));
|
||||
show_error(new_rect, format!("Second use of {what} ID {id_str}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1868,14 +1868,14 @@ impl Context {
|
||||
|
||||
let pointer_pos = self
|
||||
.pointer_hover_pos()
|
||||
.map_or_else(String::new, |pos| format!("{:?}", pos));
|
||||
ui.label(format!("Pointer pos: {}", pointer_pos));
|
||||
.map_or_else(String::new, |pos| format!("{pos:?}"));
|
||||
ui.label(format!("Pointer pos: {pointer_pos}"));
|
||||
|
||||
let top_layer = self
|
||||
.pointer_hover_pos()
|
||||
.and_then(|pos| self.layer_id_at(pos))
|
||||
.map_or_else(String::new, |layer| layer.short_debug_format());
|
||||
ui.label(format!("Top layer under mouse: {}", top_layer));
|
||||
ui.label(format!("Top layer under mouse: {top_layer}"));
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
@@ -1961,7 +1961,7 @@ impl Context {
|
||||
ui.image(texture_id, size);
|
||||
});
|
||||
|
||||
ui.label(format!("{} x {}", w, h));
|
||||
ui.label(format!("{w} x {h}"));
|
||||
ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6));
|
||||
ui.label(format!("{:?}", meta.name));
|
||||
ui.end_row();
|
||||
@@ -1982,8 +1982,7 @@ impl Context {
|
||||
|
||||
let (num_state, num_serialized) = self.data(|d| (d.len(), d.count_serialized()));
|
||||
ui.label(format!(
|
||||
"{} widget states stored (of which {} are serialized).",
|
||||
num_state, num_serialized
|
||||
"{num_state} widget states stored (of which {num_serialized} are serialized)."
|
||||
));
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -274,10 +274,10 @@ pub enum Event {
|
||||
/// Position of the touch (or where the touch was last detected)
|
||||
pos: Pos2,
|
||||
|
||||
/// Describes how hard the touch device was pressed. May always be `0` if the platform does
|
||||
/// Describes how hard the touch device was pressed. May always be `None` if the platform does
|
||||
/// not support pressure sensitivity.
|
||||
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
|
||||
force: f32,
|
||||
force: Option<f32>,
|
||||
},
|
||||
|
||||
/// A raw mouse wheel event as sent by the backend (minus the z coordinate),
|
||||
@@ -618,11 +618,11 @@ pub struct ModifierNames<'a> {
|
||||
}
|
||||
|
||||
impl ModifierNames<'static> {
|
||||
/// ⌥ ^ ⇧ ⌘ - NOTE: not supported by the default egui font.
|
||||
/// ⌥ ⌃ ⇧ ⌘ - NOTE: not supported by the default egui font.
|
||||
pub const SYMBOLS: Self = Self {
|
||||
is_short: true,
|
||||
alt: "⌥",
|
||||
ctrl: "^",
|
||||
ctrl: "⌃",
|
||||
shift: "⇧",
|
||||
mac_cmd: "⌘",
|
||||
mac_alt: "⌥",
|
||||
@@ -701,27 +701,37 @@ pub enum Key {
|
||||
|
||||
/// The virtual keycode for the Minus key.
|
||||
Minus,
|
||||
|
||||
/// The virtual keycode for the Plus/Equals key.
|
||||
PlusEquals,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num0,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num1,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num2,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num3,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num4,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num5,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num6,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num7,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num8,
|
||||
|
||||
/// Either from the main row or from the numpad.
|
||||
Num9,
|
||||
|
||||
@@ -914,7 +924,7 @@ fn format_kb_shortcut() {
|
||||
cmd_shift_f.format(&ModifierNames::NAMES, true),
|
||||
"Shift+Cmd+F"
|
||||
);
|
||||
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "^⇧F");
|
||||
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "⌃⇧F");
|
||||
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F");
|
||||
}
|
||||
|
||||
@@ -935,25 +945,25 @@ impl RawInput {
|
||||
focused,
|
||||
} = self;
|
||||
|
||||
ui.label(format!("screen_rect: {:?} points", screen_rect));
|
||||
ui.label(format!("pixels_per_point: {:?}", pixels_per_point))
|
||||
ui.label(format!("screen_rect: {screen_rect:?} points"));
|
||||
ui.label(format!("pixels_per_point: {pixels_per_point:?}"))
|
||||
.on_hover_text(
|
||||
"Also called HDPI factor.\nNumber of physical pixels per each logical pixel.",
|
||||
);
|
||||
ui.label(format!("max_texture_side: {:?}", max_texture_side));
|
||||
ui.label(format!("max_texture_side: {max_texture_side:?}"));
|
||||
if let Some(time) = time {
|
||||
ui.label(format!("time: {:.3} s", time));
|
||||
ui.label(format!("time: {time:.3} s"));
|
||||
} else {
|
||||
ui.label("time: None");
|
||||
}
|
||||
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
|
||||
ui.label(format!("modifiers: {:#?}", modifiers));
|
||||
ui.label(format!("modifiers: {modifiers:#?}"));
|
||||
ui.label(format!("hovered_files: {}", hovered_files.len()));
|
||||
ui.label(format!("dropped_files: {}", dropped_files.len()));
|
||||
ui.label(format!("focused: {}", focused));
|
||||
ui.label(format!("focused: {focused}"));
|
||||
ui.scope(|ui| {
|
||||
ui.set_min_height(150.0);
|
||||
ui.label(format!("events: {:#?}", events))
|
||||
ui.label(format!("events: {events:#?}"))
|
||||
.on_hover_text("key presses etc");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ impl PlatformOutput {
|
||||
/// This can be used by a text-to-speech system to describe the events (if any).
|
||||
pub fn events_description(&self) -> String {
|
||||
// only describe last event:
|
||||
if let Some(event) = self.events.iter().rev().next() {
|
||||
if let Some(event) = self.events.iter().next_back() {
|
||||
match event {
|
||||
OutputEvent::Clicked(widget_info)
|
||||
| OutputEvent::DoubleClicked(widget_info)
|
||||
@@ -433,12 +433,12 @@ impl OutputEvent {
|
||||
impl std::fmt::Debug for OutputEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Clicked(wi) => write!(f, "Clicked({:?})", wi),
|
||||
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi),
|
||||
Self::TripleClicked(wi) => write!(f, "TripleClicked({:?})", wi),
|
||||
Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi),
|
||||
Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({:?})", wi),
|
||||
Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", wi),
|
||||
Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
|
||||
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
|
||||
Self::TripleClicked(wi) => write!(f, "TripleClicked({wi:?})"),
|
||||
Self::FocusGained(wi) => write!(f, "FocusGained({wi:?})"),
|
||||
Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({wi:?})"),
|
||||
Self::ValueChanged(wi) => write!(f, "ValueChanged({wi:?})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -625,14 +625,14 @@ impl WidgetInfo {
|
||||
if let Some(selected) = selected {
|
||||
if *typ == WidgetType::Checkbox {
|
||||
let state = if *selected { "checked" } else { "unchecked" };
|
||||
description = format!("{} {}", state, description);
|
||||
description = format!("{state} {description}");
|
||||
} else {
|
||||
description += if *selected { "selected" } else { "" };
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(label) = label {
|
||||
description = format!("{}: {}", label, description);
|
||||
description = format!("{label}: {description}");
|
||||
}
|
||||
|
||||
if typ == &WidgetType::TextEdit {
|
||||
@@ -646,7 +646,7 @@ impl WidgetInfo {
|
||||
} else {
|
||||
text = "blank".into();
|
||||
}
|
||||
description = format!("{}: {}", text, description);
|
||||
description = format!("{text}: {description}");
|
||||
}
|
||||
|
||||
if let Some(value) = value {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crate::{id::IdSet, *};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -46,7 +44,7 @@ pub(crate) struct FrameState {
|
||||
pub(crate) scroll_delta: Vec2, // TODO(emilk): move to `InputState` ?
|
||||
|
||||
/// horizontal, vertical
|
||||
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
|
||||
pub(crate) scroll_target: [Option<(Rangef, Option<Align>)>; 2],
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) accesskit_state: Option<AccessKitFrameState>,
|
||||
|
||||
@@ -47,7 +47,7 @@ impl State {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// type alias for boxed function to determine row color during grid generation
|
||||
type ColorPickerFn = Box<dyn Fn(usize, &Style) -> Option<Color32>>;
|
||||
type ColorPickerFn = Box<dyn Send + Sync + Fn(usize, &Style) -> Option<Color32>>;
|
||||
|
||||
pub(crate) struct GridLayout {
|
||||
ctx: Context,
|
||||
@@ -60,6 +60,7 @@ pub(crate) struct GridLayout {
|
||||
/// State previous frame (if any).
|
||||
/// This can be used to predict future sizes of cells.
|
||||
prev_state: State,
|
||||
|
||||
/// State accumulated during the current frame.
|
||||
curr_state: State,
|
||||
initial_available: Rect,
|
||||
@@ -311,7 +312,7 @@ impl Grid {
|
||||
/// Setting this will allow for dynamic coloring of rows of the grid object
|
||||
pub fn with_row_color<F>(mut self, color_picker: F) -> Self
|
||||
where
|
||||
F: Fn(usize, &Style) -> Option<Color32> + 'static,
|
||||
F: Send + Sync + Fn(usize, &Style) -> Option<Color32> + 'static,
|
||||
{
|
||||
self.color_picker = Some(Box::new(color_picker));
|
||||
self
|
||||
|
||||
@@ -992,30 +992,28 @@ impl InputState {
|
||||
});
|
||||
}
|
||||
|
||||
ui.label(format!("scroll_delta: {:?} points", scroll_delta));
|
||||
ui.label(format!("zoom_factor_delta: {:4.2}x", zoom_factor_delta));
|
||||
ui.label(format!("screen_rect: {:?} points", screen_rect));
|
||||
ui.label(format!("scroll_delta: {scroll_delta:?} points"));
|
||||
ui.label(format!("zoom_factor_delta: {zoom_factor_delta:4.2}x"));
|
||||
ui.label(format!("screen_rect: {screen_rect:?} points"));
|
||||
ui.label(format!(
|
||||
"{} physical pixels for each logical point",
|
||||
pixels_per_point
|
||||
"{pixels_per_point} physical pixels for each logical point"
|
||||
));
|
||||
ui.label(format!(
|
||||
"max texture size (on each side): {}",
|
||||
max_texture_side
|
||||
"max texture size (on each side): {max_texture_side}"
|
||||
));
|
||||
ui.label(format!("time: {:.3} s", time));
|
||||
ui.label(format!("time: {time:.3} s"));
|
||||
ui.label(format!(
|
||||
"time since previous frame: {:.1} ms",
|
||||
1e3 * unstable_dt
|
||||
));
|
||||
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
|
||||
ui.label(format!("stable_dt: {:.1} ms", 1e3 * stable_dt));
|
||||
ui.label(format!("focused: {}", focused));
|
||||
ui.label(format!("modifiers: {:#?}", modifiers));
|
||||
ui.label(format!("keys_down: {:?}", keys_down));
|
||||
ui.label(format!("focused: {focused}"));
|
||||
ui.label(format!("modifiers: {modifiers:#?}"));
|
||||
ui.label(format!("keys_down: {keys_down:?}"));
|
||||
ui.scope(|ui| {
|
||||
ui.set_min_height(150.0);
|
||||
ui.label(format!("events: {:#?}", events))
|
||||
ui.label(format!("events: {events:#?}"))
|
||||
.on_hover_text("key presses etc");
|
||||
});
|
||||
}
|
||||
@@ -1039,22 +1037,21 @@ impl PointerState {
|
||||
pointer_events,
|
||||
} = self;
|
||||
|
||||
ui.label(format!("latest_pos: {:?}", latest_pos));
|
||||
ui.label(format!("interact_pos: {:?}", interact_pos));
|
||||
ui.label(format!("delta: {:?}", delta));
|
||||
ui.label(format!("latest_pos: {latest_pos:?}"));
|
||||
ui.label(format!("interact_pos: {interact_pos:?}"));
|
||||
ui.label(format!("delta: {delta:?}"));
|
||||
ui.label(format!(
|
||||
"velocity: [{:3.0} {:3.0}] points/sec",
|
||||
velocity.x, velocity.y
|
||||
));
|
||||
ui.label(format!("down: {:#?}", down));
|
||||
ui.label(format!("press_origin: {:?}", press_origin));
|
||||
ui.label(format!("press_start_time: {:?} s", press_start_time));
|
||||
ui.label(format!("down: {down:#?}"));
|
||||
ui.label(format!("press_origin: {press_origin:?}"));
|
||||
ui.label(format!("press_start_time: {press_start_time:?} s"));
|
||||
ui.label(format!(
|
||||
"has_moved_too_much_for_a_click: {}",
|
||||
has_moved_too_much_for_a_click
|
||||
"has_moved_too_much_for_a_click: {has_moved_too_much_for_a_click}"
|
||||
));
|
||||
ui.label(format!("last_click_time: {:#?}", last_click_time));
|
||||
ui.label(format!("last_last_click_time: {:#?}", last_last_click_time));
|
||||
ui.label(format!("pointer_events: {:?}", pointer_events));
|
||||
ui.label(format!("last_click_time: {last_click_time:#?}"));
|
||||
ui.label(format!("last_last_click_time: {last_last_click_time:#?}"));
|
||||
ui.label(format!("pointer_events: {pointer_events:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ struct ActiveTouch {
|
||||
///
|
||||
/// Note that a value of 0.0 either indicates a very light touch, or it means that the device
|
||||
/// is not capable of measuring the touch force.
|
||||
force: f32,
|
||||
force: Option<f32>,
|
||||
}
|
||||
|
||||
impl TouchState {
|
||||
@@ -249,7 +249,7 @@ impl TouchState {
|
||||
|
||||
// first pass: calculate force and center of touch positions:
|
||||
for touch in self.active_touches.values() {
|
||||
state.avg_force += touch.force;
|
||||
state.avg_force += touch.force.unwrap_or(0.0);
|
||||
state.avg_pos.x += touch.pos.x;
|
||||
state.avg_pos.y += touch.pos.y;
|
||||
}
|
||||
@@ -286,7 +286,7 @@ impl TouchState {
|
||||
|
||||
impl TouchState {
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
ui.label(format!("{:?}", self));
|
||||
ui.label(format!("{self:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ impl Debug for TouchState {
|
||||
// This outputs less clutter than `#[derive(Debug)]`:
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for (id, touch) in &self.active_touches {
|
||||
f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?;
|
||||
f.write_fmt(format_args!("#{id:?}: {touch:#?}\n"))?;
|
||||
}
|
||||
f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?;
|
||||
Ok(())
|
||||
|
||||
@@ -31,10 +31,7 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo
|
||||
Color32::BLACK
|
||||
};
|
||||
|
||||
ui.label(format!(
|
||||
"Texture size: {} x {} (hover to zoom)",
|
||||
width, height
|
||||
));
|
||||
ui.label(format!("Texture size: {width} x {height} (hover to zoom)"));
|
||||
if width <= 1 || height <= 1 {
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +105,7 @@ impl Widget for &epaint::stats::PaintStats {
|
||||
label(ui, shape_path, "paths");
|
||||
label(ui, shape_mesh, "nested meshes");
|
||||
label(ui, shape_vec, "nested shapes");
|
||||
ui.label(format!("{:6} callbacks", num_callbacks));
|
||||
ui.label(format!("{num_callbacks:6} callbacks"));
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.label("Text shapes:");
|
||||
|
||||
@@ -127,7 +127,7 @@ impl PaintList {
|
||||
#[inline(always)]
|
||||
pub fn add(&mut self, clip_rect: Rect, shape: Shape) -> ShapeIdx {
|
||||
let idx = ShapeIdx(self.0.len());
|
||||
self.0.push(ClippedShape(clip_rect, shape));
|
||||
self.0.push(ClippedShape { clip_rect, shape });
|
||||
idx
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ impl PaintList {
|
||||
self.0.extend(
|
||||
shapes
|
||||
.into_iter()
|
||||
.map(|shape| ClippedShape(clip_rect, shape)),
|
||||
.map(|shape| ClippedShape { clip_rect, shape }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,12 +148,12 @@ impl PaintList {
|
||||
/// and then later setting it using `paint_list.set(idx, cr, frame);`.
|
||||
#[inline(always)]
|
||||
pub fn set(&mut self, idx: ShapeIdx, clip_rect: Rect, shape: Shape) {
|
||||
self.0[idx.0] = ClippedShape(clip_rect, shape);
|
||||
self.0[idx.0] = ClippedShape { clip_rect, shape };
|
||||
}
|
||||
|
||||
/// Translate each [`Shape`] and clip rectangle by this much, in-place
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
for ClippedShape(clip_rect, shape) in &mut self.0 {
|
||||
for ClippedShape { clip_rect, shape } in &mut self.0 {
|
||||
*clip_rect = clip_rect.translate(delta);
|
||||
shape.translate(delta);
|
||||
}
|
||||
|
||||
@@ -336,7 +336,9 @@ pub use epaint::emath;
|
||||
#[cfg(feature = "color-hex")]
|
||||
pub use ecolor::hex_color;
|
||||
pub use ecolor::{Color32, Rgba};
|
||||
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
|
||||
pub use emath::{
|
||||
lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, Vec2,
|
||||
};
|
||||
pub use epaint::{
|
||||
mutex,
|
||||
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
|
||||
|
||||
@@ -604,8 +604,10 @@ impl Memory {
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Areas {
|
||||
areas: IdMap<area::State>,
|
||||
|
||||
/// Back-to-front. Top is last.
|
||||
order: Vec<LayerId>,
|
||||
|
||||
visible_last_frame: ahash::HashSet<LayerId>,
|
||||
visible_current_frame: ahash::HashSet<LayerId>,
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::ops::RangeInclusive;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
emath::{Align2, Pos2, Rect, Vec2},
|
||||
emath::{Align2, Pos2, Rangef, Rect, Vec2},
|
||||
layers::{LayerId, PaintList, ShapeIdx},
|
||||
Color32, Context, FontId,
|
||||
};
|
||||
@@ -227,7 +226,7 @@ impl Painter {
|
||||
|
||||
pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
|
||||
let color = self.ctx.style().visuals.error_fg_color;
|
||||
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {}", text))
|
||||
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
|
||||
}
|
||||
|
||||
/// text with a background
|
||||
@@ -263,12 +262,12 @@ impl Painter {
|
||||
}
|
||||
|
||||
/// Paints a horizontal line.
|
||||
pub fn hline(&self, x: RangeInclusive<f32>, y: f32, stroke: impl Into<Stroke>) {
|
||||
pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) {
|
||||
self.add(Shape::hline(x, y, stroke));
|
||||
}
|
||||
|
||||
/// Paints a vertical line.
|
||||
pub fn vline(&self, x: f32, y: RangeInclusive<f32>, stroke: impl Into<Stroke>) {
|
||||
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) {
|
||||
self.add(Shape::vline(x, y, stroke));
|
||||
}
|
||||
|
||||
|
||||
@@ -338,6 +338,13 @@ pub struct Margin {
|
||||
}
|
||||
|
||||
impl Margin {
|
||||
pub const ZERO: Self = Self {
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
};
|
||||
|
||||
#[inline]
|
||||
pub fn same(margin: f32) -> Self {
|
||||
Self {
|
||||
@@ -360,30 +367,46 @@ impl Margin {
|
||||
}
|
||||
|
||||
/// Total margins on both sides
|
||||
#[inline]
|
||||
pub fn sum(&self) -> Vec2 {
|
||||
vec2(self.left + self.right, self.top + self.bottom)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn left_top(&self) -> Vec2 {
|
||||
vec2(self.left, self.top)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn right_bottom(&self) -> Vec2 {
|
||||
vec2(self.right, self.bottom)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_same(&self) -> bool {
|
||||
self.left == self.right && self.left == self.top && self.left == self.bottom
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn expand_rect(&self, rect: Rect) -> Rect {
|
||||
Rect::from_min_max(rect.min - self.left_top(), rect.max + self.right_bottom())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn shrink_rect(&self, rect: Rect) -> Rect {
|
||||
Rect::from_min_max(rect.min + self.left_top(), rect.max - self.right_bottom())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for Margin {
|
||||
#[inline]
|
||||
fn from(v: f32) -> Self {
|
||||
Self::same(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec2> for Margin {
|
||||
#[inline]
|
||||
fn from(v: Vec2) -> Self {
|
||||
Self::symmetric(v.x, v.y)
|
||||
}
|
||||
@@ -392,6 +415,7 @@ impl From<Vec2> for Margin {
|
||||
impl std::ops::Add for Margin {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
left: self.left + other.left,
|
||||
@@ -491,7 +515,8 @@ pub struct Visuals {
|
||||
|
||||
pub resize_corner_size: f32,
|
||||
|
||||
pub text_cursor_width: f32,
|
||||
/// The color and width of the text cursor
|
||||
pub text_cursor: Stroke,
|
||||
|
||||
/// show where the text cursor would be if you clicked
|
||||
pub text_cursor_preview: bool,
|
||||
@@ -767,7 +792,7 @@ impl Visuals {
|
||||
|
||||
popup_shadow: Shadow::small_dark(),
|
||||
resize_corner_size: 12.0,
|
||||
text_cursor_width: 2.0,
|
||||
text_cursor: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)),
|
||||
text_cursor_preview: false,
|
||||
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
|
||||
button_frame: true,
|
||||
@@ -800,6 +825,7 @@ impl Visuals {
|
||||
panel_fill: Color32::from_gray(248),
|
||||
|
||||
popup_shadow: Shadow::small_light(),
|
||||
text_cursor: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)),
|
||||
..Self::dark()
|
||||
}
|
||||
}
|
||||
@@ -1334,7 +1360,7 @@ impl Visuals {
|
||||
popup_shadow,
|
||||
|
||||
resize_corner_size,
|
||||
text_cursor_width,
|
||||
text_cursor,
|
||||
text_cursor_preview,
|
||||
clip_rect_margin,
|
||||
button_frame,
|
||||
@@ -1392,8 +1418,9 @@ impl Visuals {
|
||||
});
|
||||
|
||||
ui_color(ui, hyperlink_color, "hyperlink_color");
|
||||
stroke_ui(ui, text_cursor, "Text Cursor");
|
||||
|
||||
ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
|
||||
ui.add(Slider::new(text_cursor_width, 0.0..=4.0).text("text_cursor_width"));
|
||||
ui.checkbox(text_cursor_preview, "Preview text cursor on hover");
|
||||
ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin"));
|
||||
|
||||
|
||||
@@ -517,15 +517,17 @@ impl Ui {
|
||||
}
|
||||
|
||||
/// `ui.set_width_range(min..=max);` is equivalent to `ui.set_min_width(min); ui.set_max_width(max);`.
|
||||
pub fn set_width_range(&mut self, width: std::ops::RangeInclusive<f32>) {
|
||||
self.set_min_width(*width.start());
|
||||
self.set_max_width(*width.end());
|
||||
pub fn set_width_range(&mut self, width: impl Into<Rangef>) {
|
||||
let width = width.into();
|
||||
self.set_min_width(width.min);
|
||||
self.set_max_width(width.max);
|
||||
}
|
||||
|
||||
/// `ui.set_height_range(min..=max);` is equivalent to `ui.set_min_height(min); ui.set_max_height(max);`.
|
||||
pub fn set_height_range(&mut self, height: std::ops::RangeInclusive<f32>) {
|
||||
self.set_min_height(*height.start());
|
||||
self.set_max_height(*height.end());
|
||||
pub fn set_height_range(&mut self, height: impl Into<Rangef>) {
|
||||
let height = height.into();
|
||||
self.set_min_height(height.min);
|
||||
self.set_max_height(height.max);
|
||||
}
|
||||
|
||||
/// Set both the minimum and maximum width.
|
||||
@@ -556,6 +558,7 @@ impl Ui {
|
||||
// Layout related measures:
|
||||
|
||||
/// The available space at the moment, given the current cursor.
|
||||
///
|
||||
/// This how much more space we can take up without overflowing our parent.
|
||||
/// Shrinks as widgets allocate space and the cursor moves.
|
||||
/// A small size should be interpreted as "as little as possible".
|
||||
@@ -564,19 +567,30 @@ impl Ui {
|
||||
self.placer.available_size()
|
||||
}
|
||||
|
||||
/// The available width at the moment, given the current cursor.
|
||||
///
|
||||
/// See [`Self::available_size`] for more information.
|
||||
pub fn available_width(&self) -> f32 {
|
||||
self.available_size().x
|
||||
}
|
||||
|
||||
/// The available height at the moment, given the current cursor.
|
||||
///
|
||||
/// See [`Self::available_size`] for more information.
|
||||
pub fn available_height(&self) -> f32 {
|
||||
self.available_size().y
|
||||
}
|
||||
|
||||
/// In case of a wrapping layout, how much space is left on this row/column?
|
||||
///
|
||||
/// If the layout does not wrap, this will return the same value as [`Self::available_size`].
|
||||
pub fn available_size_before_wrap(&self) -> Vec2 {
|
||||
self.placer.available_rect_before_wrap().size()
|
||||
}
|
||||
|
||||
/// In case of a wrapping layout, how much space is left on this row/column?
|
||||
///
|
||||
/// If the layout does not wrap, this will return the same value as [`Self::available_size`].
|
||||
pub fn available_rect_before_wrap(&self) -> Rect {
|
||||
self.placer.available_rect_before_wrap()
|
||||
}
|
||||
@@ -966,7 +980,7 @@ impl Ui {
|
||||
/// ```
|
||||
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
|
||||
for d in 0..2 {
|
||||
let range = rect.min[d]..=rect.max[d];
|
||||
let range = Rangef::new(rect.min[d], rect.max[d]);
|
||||
self.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d] = Some((range, align)));
|
||||
}
|
||||
@@ -996,9 +1010,9 @@ impl Ui {
|
||||
pub fn scroll_to_cursor(&self, align: Option<Align>) {
|
||||
let target = self.next_widget_position();
|
||||
for d in 0..2 {
|
||||
let target = target[d];
|
||||
let target = Rangef::point(target[d]);
|
||||
self.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d] = Some((target..=target, align)));
|
||||
.frame_state_mut(|state| state.scroll_target[d] = Some((target, align)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2231,3 +2245,9 @@ impl Ui {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_impl_send_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Ui>();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ pub type ViewportRender = dyn Fn(&Context) + Sync + Send;
|
||||
#[derive(Hash, PartialEq, Eq, Clone)]
|
||||
pub struct ViewportBuilder {
|
||||
pub title: String,
|
||||
pub name: Option<String>,
|
||||
pub name: Option<(String, String)>,
|
||||
pub position: Option<Option<(i32, i32)>>,
|
||||
pub inner_size: Option<Option<(u32, u32)>>,
|
||||
pub fullscreen: Option<bool>,
|
||||
@@ -217,6 +217,11 @@ impl ViewportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_name(mut self, id: impl Into<String>, instance: impl Into<String>) -> Self {
|
||||
self.name = Some((id.into(), instance.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Is not implemented for winit
|
||||
/// You should use `ViewportCommand::CursorHitTest` if you want to set this!
|
||||
pub fn with_hittest(mut self, value: bool) -> Self {
|
||||
|
||||
@@ -676,7 +676,7 @@ impl WidgetTextGalley {
|
||||
self.galley.size()
|
||||
}
|
||||
|
||||
/// Size of the laid out text.
|
||||
/// The full, non-elided text of the input job.
|
||||
#[inline]
|
||||
pub fn text(&self) -> &str {
|
||||
self.galley.text()
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct Button {
|
||||
text: WidgetText,
|
||||
shortcut_text: WidgetText,
|
||||
wrap: Option<bool>,
|
||||
|
||||
/// None means default for interact
|
||||
fill: Option<Color32>,
|
||||
stroke: Option<Stroke>,
|
||||
|
||||
@@ -234,17 +234,17 @@ fn color_text_ui(ui: &mut Ui, color: impl Into<Color32>, alpha: Alpha) {
|
||||
|
||||
if ui.button("📋").on_hover_text("Click to copy").clicked() {
|
||||
if alpha == Alpha::Opaque {
|
||||
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}", r, g, b));
|
||||
ui.output_mut(|o| o.copied_text = format!("{r}, {g}, {b}"));
|
||||
} else {
|
||||
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}, {}", r, g, b, a));
|
||||
ui.output_mut(|o| o.copied_text = format!("{r}, {g}, {b}, {a}"));
|
||||
}
|
||||
}
|
||||
|
||||
if alpha == Alpha::Opaque {
|
||||
ui.label(format!("rgb({}, {}, {})", r, g, b))
|
||||
ui.label(format!("rgb({r}, {g}, {b})"))
|
||||
.on_hover_text("Red Green Blue");
|
||||
} else {
|
||||
ui.label(format!("rgba({}, {}, {}, {})", r, g, b, a))
|
||||
ui.label(format!("rgba({r}, {g}, {b}, {a})"))
|
||||
.on_hover_text("Red Green Blue with premultiplied Alpha");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::*;
|
||||
pub(crate) struct MonoState {
|
||||
last_dragged_id: Option<Id>,
|
||||
last_dragged_value: Option<f64>,
|
||||
|
||||
/// For temporary edit of a [`DragValue`] value.
|
||||
/// Couples with the current focus id.
|
||||
edit_string: Option<String>,
|
||||
@@ -63,6 +64,7 @@ pub struct DragValue<'a> {
|
||||
max_decimals: Option<usize>,
|
||||
custom_formatter: Option<NumFormatter<'a>>,
|
||||
custom_parser: Option<NumParser<'a>>,
|
||||
update_while_editing: bool,
|
||||
}
|
||||
|
||||
impl<'a> DragValue<'a> {
|
||||
@@ -94,6 +96,7 @@ impl<'a> DragValue<'a> {
|
||||
max_decimals: None,
|
||||
custom_formatter: None,
|
||||
custom_parser: None,
|
||||
update_while_editing: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +355,15 @@ impl<'a> DragValue<'a> {
|
||||
}
|
||||
.custom_parser(|s| i64::from_str_radix(s, 16).map(|n| n as f64).ok())
|
||||
}
|
||||
|
||||
/// Update the value on each key press when text-editing the value.
|
||||
///
|
||||
/// Default: `true`.
|
||||
/// If `false`, the value will only be updated when user presses enter or deselects the value.
|
||||
pub fn update_while_editing(mut self, update: bool) -> Self {
|
||||
self.update_while_editing = update;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for DragValue<'a> {
|
||||
@@ -366,6 +378,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||
max_decimals,
|
||||
custom_formatter,
|
||||
custom_parser,
|
||||
update_while_editing,
|
||||
} = self;
|
||||
|
||||
let shift = ui.input(|i| i.modifiers.shift_only());
|
||||
@@ -392,7 +405,9 @@ impl<'a> Widget for DragValue<'a> {
|
||||
|
||||
let auto_decimals = (aim_rad / speed.abs()).log10().ceil().clamp(0.0, 15.0) as usize;
|
||||
let auto_decimals = auto_decimals + is_slow_speed as usize;
|
||||
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
|
||||
let max_decimals = max_decimals
|
||||
.unwrap_or(auto_decimals + 2)
|
||||
.at_least(min_decimals);
|
||||
let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
|
||||
|
||||
let change = ui.input_mut(|input| {
|
||||
@@ -475,9 +490,15 @@ impl<'a> Widget for DragValue<'a> {
|
||||
.desired_width(ui.spacing().interact_size.x)
|
||||
.font(text_style),
|
||||
);
|
||||
// Only update the value when the user presses enter, or clicks elsewhere. NOT every frame.
|
||||
// See https://github.com/emilk/egui/issues/2687
|
||||
if response.lost_focus() {
|
||||
|
||||
let update = if update_while_editing {
|
||||
// Update when the edit content has changed.
|
||||
response.changed()
|
||||
} else {
|
||||
// Update only when the edit has lost focus.
|
||||
response.lost_focus()
|
||||
};
|
||||
if update {
|
||||
let parsed_value = match custom_parser {
|
||||
Some(parser) => parser(&value_text),
|
||||
None => value_text.parse().ok(),
|
||||
@@ -606,7 +627,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||
// The value is exposed as a string by the text edit widget
|
||||
// when in edit mode.
|
||||
if !is_kb_editing {
|
||||
let value_text = format!("{}{}{}", prefix, value_text, suffix);
|
||||
let value_text = format!("{prefix}{value_text}{suffix}");
|
||||
builder.set_value(value_text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -83,6 +83,7 @@ impl Widget for Link {
|
||||
pub struct Hyperlink {
|
||||
url: String,
|
||||
text: WidgetText,
|
||||
new_tab: bool,
|
||||
}
|
||||
|
||||
impl Hyperlink {
|
||||
@@ -92,6 +93,7 @@ impl Hyperlink {
|
||||
Self {
|
||||
url: url.clone(),
|
||||
text: url.into(),
|
||||
new_tab: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,13 +102,20 @@ impl Hyperlink {
|
||||
Self {
|
||||
url: url.to_string(),
|
||||
text: text.into(),
|
||||
new_tab: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Always open this hyperlink in a new browser tab.
|
||||
pub fn open_in_new_tab(mut self, new_tab: bool) -> Self {
|
||||
self.new_tab = new_tab;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Hyperlink {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let Self { url, text } = self;
|
||||
let Self { url, text, new_tab } = self;
|
||||
|
||||
let response = ui.add(Link::new(text));
|
||||
if response.clicked() {
|
||||
@@ -114,7 +123,7 @@ impl Widget for Hyperlink {
|
||||
ui.ctx().output_mut(|o| {
|
||||
o.open_url = Some(crate::output::OpenUrl {
|
||||
url: url.clone(),
|
||||
new_tab: modifiers.any(),
|
||||
new_tab: new_tab || modifiers.any(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,10 +12,14 @@ use crate::{widget_text::WidgetTextGalley, *};
|
||||
/// ui.label(egui::RichText::new("With formatting").underline());
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// For full control of the text you can use [`crate::text::LayoutJob`]
|
||||
/// as argument to [`Self::new`].
|
||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||
pub struct Label {
|
||||
text: WidgetText,
|
||||
wrap: Option<bool>,
|
||||
truncate: bool,
|
||||
sense: Option<Sense>,
|
||||
}
|
||||
|
||||
@@ -24,6 +28,7 @@ impl Label {
|
||||
Self {
|
||||
text: text.into(),
|
||||
wrap: None,
|
||||
truncate: false,
|
||||
sense: None,
|
||||
}
|
||||
}
|
||||
@@ -34,6 +39,8 @@ impl Label {
|
||||
|
||||
/// If `true`, the text will wrap to stay within the max width of the [`Ui`].
|
||||
///
|
||||
/// Calling `wrap` will override [`Self::truncate`].
|
||||
///
|
||||
/// By default [`Self::wrap`] will be `true` in vertical layouts
|
||||
/// and horizontal layouts with wrapping,
|
||||
/// and `false` on non-wrapping horizontal layouts.
|
||||
@@ -44,6 +51,23 @@ impl Label {
|
||||
#[inline]
|
||||
pub fn wrap(mut self, wrap: bool) -> Self {
|
||||
self.wrap = Some(wrap);
|
||||
self.truncate = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// If `true`, the text will stop at the max width of the [`Ui`],
|
||||
/// and what doesn't fit will be elided, replaced with `…`.
|
||||
///
|
||||
/// If the text is truncated, the full text will be shown on hover as a tool-tip.
|
||||
///
|
||||
/// Default is `false`, which means the text will expand the parent [`Ui`],
|
||||
/// or wrap if [`Self::wrap`] is set.
|
||||
///
|
||||
/// Calling `truncate` will override [`Self::wrap`].
|
||||
#[inline]
|
||||
pub fn truncate(mut self, truncate: bool) -> Self {
|
||||
self.wrap = None;
|
||||
self.truncate = truncate;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -98,10 +122,11 @@ impl Label {
|
||||
.text
|
||||
.into_text_job(ui.style(), FontSelection::Default, valign);
|
||||
|
||||
let should_wrap = self.wrap.unwrap_or_else(|| ui.wrap_text());
|
||||
let truncate = self.truncate;
|
||||
let wrap = !truncate && self.wrap.unwrap_or_else(|| ui.wrap_text());
|
||||
let available_width = ui.available_width();
|
||||
|
||||
if should_wrap
|
||||
if wrap
|
||||
&& ui.layout().main_dir() == Direction::LeftToRight
|
||||
&& ui.layout().main_wrap()
|
||||
&& available_width.is_finite()
|
||||
@@ -138,7 +163,11 @@ impl Label {
|
||||
}
|
||||
(pos, text_galley, response)
|
||||
} else {
|
||||
if should_wrap {
|
||||
if truncate {
|
||||
text_job.job.wrap.max_width = available_width;
|
||||
text_job.job.wrap.max_rows = 1;
|
||||
text_job.job.wrap.break_anywhere = true;
|
||||
} else if wrap {
|
||||
text_job.job.wrap.max_width = available_width;
|
||||
} else {
|
||||
text_job.job.wrap.max_width = f32::INFINITY;
|
||||
@@ -167,9 +196,14 @@ impl Label {
|
||||
|
||||
impl Widget for Label {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let (pos, text_galley, response) = self.layout_in_ui(ui);
|
||||
let (pos, text_galley, mut response) = self.layout_in_ui(ui);
|
||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, text_galley.text()));
|
||||
|
||||
if text_galley.galley.elided {
|
||||
// Show the full (non-elided) text on hover:
|
||||
response = response.on_hover_text(text_galley.text());
|
||||
}
|
||||
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
let response_color = ui.style().interact(&response).text_color();
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ pub use text_edit::{TextBuffer, TextEdit};
|
||||
///
|
||||
/// [`Button`], [`Label`], [`Slider`], etc all implement the [`Widget`] trait.
|
||||
///
|
||||
/// You only need to implement `Widget` if you care about being able to do `ui.add(your_widget);`.
|
||||
///
|
||||
/// Note that the widgets ([`Button`], [`TextEdit`] etc) are
|
||||
/// [builders](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html),
|
||||
/// and not objects that hold state.
|
||||
|
||||
318
crates/egui/src/widgets/plot/axis.rs
Normal file
318
crates/egui/src/widgets/plot/axis.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
use std::{fmt::Debug, ops::RangeInclusive, sync::Arc};
|
||||
|
||||
use epaint::{
|
||||
emath::{remap_clamp, round_to_decimals},
|
||||
Pos2, Rect, Shape, Stroke, TextShape,
|
||||
};
|
||||
|
||||
use crate::{Response, Sense, TextStyle, Ui, WidgetText};
|
||||
|
||||
use super::{transform::PlotTransform, GridMark};
|
||||
|
||||
pub(super) type AxisFormatterFn = fn(f64, usize, &RangeInclusive<f64>) -> String;
|
||||
|
||||
/// X or Y axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Axis {
|
||||
/// Horizontal X-Axis
|
||||
X,
|
||||
|
||||
/// Vertical Y-axis
|
||||
Y,
|
||||
}
|
||||
|
||||
impl From<Axis> for usize {
|
||||
#[inline]
|
||||
fn from(value: Axis) -> Self {
|
||||
match value {
|
||||
Axis::X => 0,
|
||||
Axis::Y => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Placement of the horizontal X-Axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VPlacement {
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
/// Placement of the vertical Y-Axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HPlacement {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Placement of an axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Placement {
|
||||
/// Bottom for X-axis, or left for Y-axis.
|
||||
LeftBottom,
|
||||
|
||||
/// Top for x-axis and right for y-axis.
|
||||
RightTop,
|
||||
}
|
||||
|
||||
impl From<HPlacement> for Placement {
|
||||
#[inline]
|
||||
fn from(placement: HPlacement) -> Self {
|
||||
match placement {
|
||||
HPlacement::Left => Placement::LeftBottom,
|
||||
HPlacement::Right => Placement::RightTop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VPlacement> for Placement {
|
||||
#[inline]
|
||||
fn from(placement: VPlacement) -> Self {
|
||||
match placement {
|
||||
VPlacement::Top => Placement::RightTop,
|
||||
VPlacement::Bottom => Placement::LeftBottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Axis configuration.
|
||||
///
|
||||
/// Used to configure axis label and ticks.
|
||||
#[derive(Clone)]
|
||||
pub struct AxisHints {
|
||||
pub(super) label: WidgetText,
|
||||
pub(super) formatter: AxisFormatterFn,
|
||||
pub(super) digits: usize,
|
||||
pub(super) placement: Placement,
|
||||
}
|
||||
|
||||
// TODO: this just a guess. It might cease to work if a user changes font size.
|
||||
const LINE_HEIGHT: f32 = 12.0;
|
||||
|
||||
impl Default for AxisHints {
|
||||
/// Initializes a default axis configuration for the specified axis.
|
||||
///
|
||||
/// `label` is empty.
|
||||
/// `formatter` is default float to string formatter.
|
||||
/// maximum `digits` on tick label is 5.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
label: Default::default(),
|
||||
formatter: Self::default_formatter,
|
||||
digits: 5,
|
||||
placement: Placement::LeftBottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AxisHints {
|
||||
/// Specify custom formatter for ticks.
|
||||
///
|
||||
/// The first parameter of `formatter` is the raw tick value as `f64`.
|
||||
/// The second parameter is the maximum number of characters that fit into y-labels.
|
||||
/// The second parameter of `formatter` is the currently shown range on this axis.
|
||||
pub fn formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
|
||||
self.formatter = fmt;
|
||||
self
|
||||
}
|
||||
|
||||
fn default_formatter(tick: f64, max_digits: usize, _range: &RangeInclusive<f64>) -> String {
|
||||
if tick.abs() > 10.0_f64.powf(max_digits as f64) {
|
||||
let tick_rounded = tick as isize;
|
||||
return format!("{tick_rounded:+e}");
|
||||
}
|
||||
let tick_rounded = round_to_decimals(tick, max_digits);
|
||||
if tick.abs() < 10.0_f64.powf(-(max_digits as f64)) && tick != 0.0 {
|
||||
return format!("{tick_rounded:+e}");
|
||||
}
|
||||
tick_rounded.to_string()
|
||||
}
|
||||
|
||||
/// Specify axis label.
|
||||
///
|
||||
/// The default is 'x' for x-axes and 'y' for y-axes.
|
||||
pub fn label(mut self, label: impl Into<WidgetText>) -> Self {
|
||||
self.label = label.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify maximum number of digits for ticks.
|
||||
///
|
||||
/// This is considered by the default tick formatter and affects the width of the y-axis
|
||||
pub fn max_digits(mut self, digits: usize) -> Self {
|
||||
self.digits = digits;
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the placement of the axis.
|
||||
///
|
||||
/// For X-axis, use [`VPlacement`].
|
||||
/// For Y-axis, use [`HPlacement`].
|
||||
pub fn placement(mut self, placement: impl Into<Placement>) -> Self {
|
||||
self.placement = placement.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn thickness(&self, axis: Axis) -> f32 {
|
||||
match axis {
|
||||
Axis::X => {
|
||||
if self.label.is_empty() {
|
||||
1.0 * LINE_HEIGHT
|
||||
} else {
|
||||
3.0 * LINE_HEIGHT
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
if self.label.is_empty() {
|
||||
(self.digits as f32) * LINE_HEIGHT
|
||||
} else {
|
||||
(self.digits as f32 + 1.0) * LINE_HEIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct AxisWidget {
|
||||
pub(super) range: RangeInclusive<f64>,
|
||||
pub(super) hints: AxisHints,
|
||||
pub(super) rect: Rect,
|
||||
pub(super) transform: Option<PlotTransform>,
|
||||
pub(super) steps: Arc<Vec<GridMark>>,
|
||||
}
|
||||
|
||||
impl AxisWidget {
|
||||
/// if `rect` as width or height == 0, is will be automatically calculated from ticks and text.
|
||||
pub(super) fn new(hints: AxisHints, rect: Rect) -> Self {
|
||||
Self {
|
||||
range: (0.0..=0.0),
|
||||
hints,
|
||||
rect,
|
||||
transform: None,
|
||||
steps: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(self, ui: &mut Ui, axis: Axis) -> Response {
|
||||
let response = ui.allocate_rect(self.rect, Sense::hover());
|
||||
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
let visuals = ui.style().visuals.clone();
|
||||
let text = self.hints.label;
|
||||
let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body);
|
||||
let text_color = visuals
|
||||
.override_text_color
|
||||
.unwrap_or_else(|| ui.visuals().text_color());
|
||||
let angle: f32 = match axis {
|
||||
Axis::X => 0.0,
|
||||
Axis::Y => -std::f32::consts::TAU * 0.25,
|
||||
};
|
||||
// select text_pos and angle depending on placement and orientation of widget
|
||||
let text_pos = match self.hints.placement {
|
||||
Placement::LeftBottom => match axis {
|
||||
Axis::X => {
|
||||
let pos = response.rect.center_bottom();
|
||||
Pos2 {
|
||||
x: pos.x - galley.size().x / 2.0,
|
||||
y: pos.y - galley.size().y * 1.25,
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let pos = response.rect.left_center();
|
||||
Pos2 {
|
||||
x: pos.x,
|
||||
y: pos.y + galley.size().x / 2.0,
|
||||
}
|
||||
}
|
||||
},
|
||||
Placement::RightTop => match axis {
|
||||
Axis::X => {
|
||||
let pos = response.rect.center_top();
|
||||
Pos2 {
|
||||
x: pos.x - galley.size().x / 2.0,
|
||||
y: pos.y + galley.size().y * 0.25,
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let pos = response.rect.right_center();
|
||||
Pos2 {
|
||||
x: pos.x - galley.size().y * 1.5,
|
||||
y: pos.y + galley.size().x / 2.0,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
let shape = TextShape {
|
||||
pos: text_pos,
|
||||
galley: galley.galley,
|
||||
underline: Stroke::NONE,
|
||||
override_text_color: Some(text_color),
|
||||
angle,
|
||||
};
|
||||
ui.painter().add(shape);
|
||||
|
||||
// --- add ticks ---
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let transform = match self.transform {
|
||||
Some(t) => t,
|
||||
None => return response,
|
||||
};
|
||||
|
||||
for step in self.steps.iter() {
|
||||
let text = (self.hints.formatter)(step.value, self.hints.digits, &self.range);
|
||||
if !text.is_empty() {
|
||||
const MIN_TEXT_SPACING: f32 = 20.0;
|
||||
const FULL_CONTRAST_SPACING: f32 = 40.0;
|
||||
let spacing_in_points =
|
||||
(transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
|
||||
|
||||
if spacing_in_points <= MIN_TEXT_SPACING {
|
||||
continue;
|
||||
}
|
||||
let line_strength = remap_clamp(
|
||||
spacing_in_points,
|
||||
MIN_TEXT_SPACING..=FULL_CONTRAST_SPACING,
|
||||
0.0..=1.0,
|
||||
);
|
||||
|
||||
let line_color = super::color_from_strength(ui, line_strength);
|
||||
let galley = ui
|
||||
.painter()
|
||||
.layout_no_wrap(text, font_id.clone(), line_color);
|
||||
|
||||
let text_pos = match axis {
|
||||
Axis::X => {
|
||||
let y = match self.hints.placement {
|
||||
Placement::LeftBottom => self.rect.min.y,
|
||||
Placement::RightTop => self.rect.max.y - galley.size().y,
|
||||
};
|
||||
let projected_point = super::PlotPoint::new(step.value, 0.0);
|
||||
Pos2 {
|
||||
x: transform.position_from_point(&projected_point).x
|
||||
- galley.size().x / 2.0,
|
||||
y,
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let x = match self.hints.placement {
|
||||
Placement::LeftBottom => self.rect.max.x - galley.size().x,
|
||||
Placement::RightTop => self.rect.min.x,
|
||||
};
|
||||
let projected_point = super::PlotPoint::new(0.0, step.value);
|
||||
Pos2 {
|
||||
x,
|
||||
y: transform.position_from_point(&projected_point).y
|
||||
- galley.size().y / 2.0,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ui.painter().add(Shape::galley(text_pos, galley));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
@@ -760,15 +760,22 @@ impl PlotItem for Text {
|
||||
/// A set of points.
|
||||
pub struct Points {
|
||||
pub(super) series: PlotPoints,
|
||||
|
||||
pub(super) shape: MarkerShape,
|
||||
|
||||
/// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically.
|
||||
pub(super) color: Color32,
|
||||
|
||||
/// Whether to fill the marker. Does not apply to all types.
|
||||
pub(super) filled: bool,
|
||||
|
||||
/// The maximum extent of the marker from its center.
|
||||
pub(super) radius: f32,
|
||||
|
||||
pub(super) name: String,
|
||||
|
||||
pub(super) highlight: bool,
|
||||
|
||||
pub(super) stems: Option<f32>,
|
||||
}
|
||||
|
||||
@@ -997,6 +1004,7 @@ impl PlotItem for Points {
|
||||
pub struct Arrows {
|
||||
pub(super) origins: PlotPoints,
|
||||
pub(super) tips: PlotPoints,
|
||||
pub(super) tip_length: Option<f32>,
|
||||
pub(super) color: Color32,
|
||||
pub(super) name: String,
|
||||
pub(super) highlight: bool,
|
||||
@@ -1007,6 +1015,7 @@ impl Arrows {
|
||||
Self {
|
||||
origins: origins.into(),
|
||||
tips: tips.into(),
|
||||
tip_length: None,
|
||||
color: Color32::TRANSPARENT,
|
||||
name: Default::default(),
|
||||
highlight: false,
|
||||
@@ -1019,6 +1028,12 @@ impl Arrows {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the length of the arrow tips
|
||||
pub fn tip_length(mut self, tip_length: f32) -> Self {
|
||||
self.tip_length = Some(tip_length);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the arrows' color.
|
||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||
self.color = color.into();
|
||||
@@ -1044,6 +1059,7 @@ impl PlotItem for Arrows {
|
||||
let Self {
|
||||
origins,
|
||||
tips,
|
||||
tip_length,
|
||||
color,
|
||||
highlight,
|
||||
..
|
||||
@@ -1062,7 +1078,11 @@ impl PlotItem for Arrows {
|
||||
.for_each(|(origin, tip)| {
|
||||
let vector = tip - origin;
|
||||
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
|
||||
let tip_length = vector.length() / 4.0;
|
||||
let tip_length = if let Some(tip_length) = tip_length {
|
||||
*tip_length
|
||||
} else {
|
||||
vector.length() / 4.0
|
||||
};
|
||||
let tip = origin + vector;
|
||||
let dir = vector.normalized();
|
||||
shapes.push(Shape::line_segment([origin, tip], stroke));
|
||||
@@ -1119,6 +1139,7 @@ pub struct PlotImage {
|
||||
pub(super) tint: Color32,
|
||||
pub(super) highlight: bool,
|
||||
pub(super) name: String,
|
||||
pub(crate) rotation: Option<(f32, Vec2)>,
|
||||
}
|
||||
|
||||
impl PlotImage {
|
||||
@@ -1137,6 +1158,7 @@ impl PlotImage {
|
||||
size: size.into(),
|
||||
bg_fill: Default::default(),
|
||||
tint: Color32::WHITE,
|
||||
rotation: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1175,6 +1197,17 @@ impl PlotImage {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Rotate the image about an origin by some angle
|
||||
///
|
||||
/// Positive angle is clockwise.
|
||||
/// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right).
|
||||
///
|
||||
/// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin.
|
||||
pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
|
||||
self.rotation = Some((angle, origin));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotItem for PlotImage {
|
||||
@@ -1202,11 +1235,14 @@ impl PlotItem for PlotImage {
|
||||
let right_bottom_tf = transform.position_from_point(&right_bottom);
|
||||
Rect::from_two_pos(left_top_tf, right_bottom_tf)
|
||||
};
|
||||
Image::new(*texture_id, *size)
|
||||
let mut image = Image::new(*texture_id, *size)
|
||||
.bg_fill(*bg_fill)
|
||||
.tint(*tint)
|
||||
.uv(*uv)
|
||||
.paint_at(ui, rect);
|
||||
.uv(*uv);
|
||||
if let Some((angle, origin)) = self.rotation {
|
||||
image = image.rotate(angle, origin);
|
||||
}
|
||||
image.paint_at(ui, rect);
|
||||
if *highlight {
|
||||
shapes.push(Shape::rect_stroke(
|
||||
rect,
|
||||
@@ -1261,8 +1297,10 @@ pub struct BarChart {
|
||||
pub(super) bars: Vec<Bar>,
|
||||
pub(super) default_color: Color32,
|
||||
pub(super) name: String,
|
||||
|
||||
/// A custom element formatter
|
||||
pub(super) element_formatter: Option<Box<dyn Fn(&Bar, &BarChart) -> String>>,
|
||||
|
||||
highlight: bool,
|
||||
}
|
||||
|
||||
@@ -1431,8 +1469,10 @@ pub struct BoxPlot {
|
||||
pub(super) boxes: Vec<BoxElem>,
|
||||
pub(super) default_color: Color32,
|
||||
pub(super) name: String,
|
||||
|
||||
/// A custom element formatter
|
||||
pub(super) element_formatter: Option<Box<dyn Fn(&BoxElem, &BoxPlot) -> String>>,
|
||||
|
||||
highlight: bool,
|
||||
}
|
||||
|
||||
@@ -1692,7 +1732,7 @@ pub(super) fn rulers_at_value(
|
||||
let mut prefix = String::new();
|
||||
|
||||
if !name.is_empty() {
|
||||
prefix = format!("{}\n", name);
|
||||
prefix = format!("{name}\n");
|
||||
}
|
||||
|
||||
let text = {
|
||||
|
||||
@@ -125,8 +125,8 @@ impl ToString for LineStyle {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
LineStyle::Solid => "Solid".into(),
|
||||
LineStyle::Dotted { spacing } => format!("Dotted{}Px", spacing),
|
||||
LineStyle::Dashed { length } => format!("Dashed{}Px", length),
|
||||
LineStyle::Dotted { spacing } => format!("Dotted{spacing}Px"),
|
||||
LineStyle::Dashed { length } => format!("Dashed{length}Px"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
crates/egui/src/widgets/plot/memory.rs
Normal file
33
crates/egui/src/widgets/plot/memory.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use epaint::Pos2;
|
||||
|
||||
use crate::{Context, Id};
|
||||
|
||||
use super::{transform::ScreenTransform, AxisBools};
|
||||
|
||||
/// Information about the plot that has to persist between frames.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone)]
|
||||
pub(super) struct PlotMemory {
|
||||
/// Indicates if the user has modified the bounds, for example by moving or zooming,
|
||||
/// or if the bounds should be calculated based by included point or auto bounds.
|
||||
pub(super) bounds_modified: AxisBools,
|
||||
|
||||
pub(super) hovered_entry: Option<String>,
|
||||
|
||||
pub(super) hidden_items: ahash::HashSet<String>,
|
||||
|
||||
pub(super) last_screen_transform: ScreenTransform,
|
||||
|
||||
/// Allows to remember the first click position when performing a boxed zoom
|
||||
pub(super) last_click_pos_for_zoom: Option<Pos2>,
|
||||
}
|
||||
|
||||
impl PlotMemory {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data().get_persisted(id)
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
ctx.data().insert_persisted(id, self);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
//! Simple plotting library.
|
||||
|
||||
use ahash::HashMap;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::{ops::RangeInclusive, sync::Arc};
|
||||
|
||||
use crate::*;
|
||||
use ahash::HashMap;
|
||||
use epaint::util::FloatOrd;
|
||||
use epaint::Hsva;
|
||||
|
||||
use axis::AxisWidget;
|
||||
use items::PlotItem;
|
||||
use legend::LegendWidget;
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub use items::{
|
||||
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
|
||||
Orientation, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Text, VLine,
|
||||
@@ -17,16 +19,17 @@ pub use items::{
|
||||
pub use legend::{Corner, Legend};
|
||||
pub use transform::{PlotBounds, PlotTransform};
|
||||
|
||||
use self::items::{horizontal_line, rulers_color, vertical_line};
|
||||
use items::{horizontal_line, rulers_color, vertical_line};
|
||||
|
||||
pub use axis::{Axis, AxisHints, HPlacement, Placement, VPlacement};
|
||||
|
||||
mod axis;
|
||||
mod items;
|
||||
mod legend;
|
||||
mod transform;
|
||||
|
||||
type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
|
||||
type LabelFormatter = Option<Box<LabelFormatterFn>>;
|
||||
type AxisFormatterFn = dyn Fn(f64, &RangeInclusive<f64>) -> String;
|
||||
type AxisFormatter = Option<Box<AxisFormatterFn>>;
|
||||
|
||||
type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
|
||||
type GridSpacer = Box<GridSpacerFn>;
|
||||
@@ -78,6 +81,7 @@ pub struct AxisBools {
|
||||
}
|
||||
|
||||
impl AxisBools {
|
||||
#[inline]
|
||||
pub fn new(x: bool, y: bool) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
@@ -89,11 +93,19 @@ impl AxisBools {
|
||||
}
|
||||
|
||||
impl From<bool> for AxisBools {
|
||||
#[inline]
|
||||
fn from(val: bool) -> Self {
|
||||
AxisBools { x: val, y: val }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[bool; 2]> for AxisBools {
|
||||
#[inline]
|
||||
fn from([x, y]: [bool; 2]) -> Self {
|
||||
AxisBools { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the plot that has to persist between frames.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone)]
|
||||
@@ -101,9 +113,11 @@ struct PlotMemory {
|
||||
/// Indicates if the user has modified the bounds, for example by moving or zooming,
|
||||
/// or if the bounds should be calculated based by included point or auto bounds.
|
||||
bounds_modified: AxisBools,
|
||||
|
||||
hovered_entry: Option<String>,
|
||||
hidden_items: ahash::HashSet<String>,
|
||||
last_plot_transform: PlotTransform,
|
||||
|
||||
/// Allows to remember the first click position when performing a boxed zoom
|
||||
last_click_pos_for_zoom: Option<Pos2>,
|
||||
}
|
||||
@@ -180,8 +194,7 @@ pub struct PlotResponse<R> {
|
||||
pub struct Plot {
|
||||
id_source: Id,
|
||||
|
||||
center_x_axis: bool,
|
||||
center_y_axis: bool,
|
||||
center_axis: AxisBools,
|
||||
allow_zoom: AxisBools,
|
||||
allow_drag: AxisBools,
|
||||
allow_scroll: bool,
|
||||
@@ -206,11 +219,12 @@ pub struct Plot {
|
||||
show_y: bool,
|
||||
label_formatter: LabelFormatter,
|
||||
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
||||
axis_formatters: [AxisFormatter; 2],
|
||||
x_axes: Vec<AxisHints>, // default x axes
|
||||
y_axes: Vec<AxisHints>, // default y axes
|
||||
legend_config: Option<Legend>,
|
||||
show_background: bool,
|
||||
show_axes: [bool; 2],
|
||||
|
||||
show_axes: AxisBools,
|
||||
show_grid: AxisBools,
|
||||
grid_spacers: [GridSpacer; 2],
|
||||
sharp_grid_lines: bool,
|
||||
clamp_grid: bool,
|
||||
@@ -222,8 +236,7 @@ impl Plot {
|
||||
Self {
|
||||
id_source: Id::new(id_source),
|
||||
|
||||
center_x_axis: false,
|
||||
center_y_axis: false,
|
||||
center_axis: false.into(),
|
||||
allow_zoom: true.into(),
|
||||
allow_drag: true.into(),
|
||||
allow_scroll: true,
|
||||
@@ -248,11 +261,12 @@ impl Plot {
|
||||
show_y: true,
|
||||
label_formatter: None,
|
||||
coordinates_formatter: None,
|
||||
axis_formatters: [None, None], // [None; 2] requires Copy
|
||||
x_axes: vec![Default::default()],
|
||||
y_axes: vec![Default::default()],
|
||||
legend_config: None,
|
||||
show_background: true,
|
||||
show_axes: [true; 2],
|
||||
|
||||
show_axes: true.into(),
|
||||
show_grid: true.into(),
|
||||
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
|
||||
sharp_grid_lines: true,
|
||||
clamp_grid: false,
|
||||
@@ -309,15 +323,15 @@ impl Plot {
|
||||
self
|
||||
}
|
||||
|
||||
/// Always keep the x-axis centered. Default: `false`.
|
||||
/// Always keep the X-axis centered. Default: `false`.
|
||||
pub fn center_x_axis(mut self, on: bool) -> Self {
|
||||
self.center_x_axis = on;
|
||||
self.center_axis.x = on;
|
||||
self
|
||||
}
|
||||
|
||||
/// Always keep the y-axis centered. Default: `false`.
|
||||
/// Always keep the Y-axis centered. Default: `false`.
|
||||
pub fn center_y_axis(mut self, on: bool) -> Self {
|
||||
self.center_y_axis = on;
|
||||
self.center_axis.y = on;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -415,36 +429,6 @@ impl Plot {
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a function to customize the labels for the X axis based on the current visible value range.
|
||||
///
|
||||
/// This is useful for custom input domains, e.g. date/time.
|
||||
///
|
||||
/// If axis labels should not appear for certain values or beyond a certain zoom/resolution,
|
||||
/// the formatter function can return empty strings. This is also useful if your domain is
|
||||
/// discrete (e.g. only full days in a calendar).
|
||||
pub fn x_axis_formatter(
|
||||
mut self,
|
||||
func: impl Fn(f64, &RangeInclusive<f64>) -> String + 'static,
|
||||
) -> Self {
|
||||
self.axis_formatters[0] = Some(Box::new(func));
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a function to customize the labels for the Y axis based on the current value range.
|
||||
///
|
||||
/// This is useful for custom value representation, e.g. percentage or units.
|
||||
///
|
||||
/// If axis labels should not appear for certain values or beyond a certain zoom/resolution,
|
||||
/// the formatter function can return empty strings. This is also useful if your Y values are
|
||||
/// discrete (e.g. only integers).
|
||||
pub fn y_axis_formatter(
|
||||
mut self,
|
||||
func: impl Fn(f64, &RangeInclusive<f64>) -> String + 'static,
|
||||
) -> Self {
|
||||
self.axis_formatters[1] = Some(Box::new(func));
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure how the grid in the background is spaced apart along the X axis.
|
||||
///
|
||||
/// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
|
||||
@@ -536,11 +520,19 @@ impl Plot {
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the axes.
|
||||
/// Can be useful to disable if the plot is overlaid over an existing grid or content.
|
||||
/// Show axis labels and grid tick values on the side of the plot.
|
||||
///
|
||||
/// Default: `[true; 2]`.
|
||||
pub fn show_axes(mut self, show: [bool; 2]) -> Self {
|
||||
self.show_axes = show;
|
||||
pub fn show_axes(mut self, show: impl Into<AxisBools>) -> Self {
|
||||
self.show_axes = show.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Show a grid overlay on the plot.
|
||||
///
|
||||
/// Default: `[true; 2]`.
|
||||
pub fn show_grid(mut self, show: impl Into<AxisBools>) -> Self {
|
||||
self.show_grid = show.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -583,6 +575,94 @@ impl Plot {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the x axis label of the main X-axis.
|
||||
///
|
||||
/// Default: no label.
|
||||
pub fn x_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
|
||||
if let Some(main) = self.x_axes.first_mut() {
|
||||
main.label = label.into();
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the y axis label of the main Y-axis.
|
||||
///
|
||||
/// Default: no label.
|
||||
pub fn y_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
|
||||
if let Some(main) = self.y_axes.first_mut() {
|
||||
main.label = label.into();
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the position of the main X-axis.
|
||||
pub fn x_axis_position(mut self, placement: axis::VPlacement) -> Self {
|
||||
if let Some(main) = self.x_axes.first_mut() {
|
||||
main.placement = placement.into();
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the position of the main Y-axis.
|
||||
pub fn y_axis_position(mut self, placement: axis::HPlacement) -> Self {
|
||||
if let Some(main) = self.y_axes.first_mut() {
|
||||
main.placement = placement.into();
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify custom formatter for ticks on the main X-axis.
|
||||
///
|
||||
/// The first parameter of `fmt` is the raw tick value as `f64`.
|
||||
/// The second parameter is the maximum requested number of characters per tick label.
|
||||
/// The second parameter of `fmt` is the currently shown range on this axis.
|
||||
pub fn x_axis_formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
|
||||
if let Some(main) = self.x_axes.first_mut() {
|
||||
main.formatter = fmt;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify custom formatter for ticks on the main Y-axis.
|
||||
///
|
||||
/// The first parameter of `formatter` is the raw tick value as `f64`.
|
||||
/// The second parameter is the maximum requested number of characters per tick label.
|
||||
/// The second parameter of `formatter` is the currently shown range on this axis.
|
||||
pub fn y_axis_formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
|
||||
if let Some(main) = self.y_axes.first_mut() {
|
||||
main.formatter = fmt;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the main Y-axis-width by number of digits
|
||||
///
|
||||
/// The default is 5 digits.
|
||||
///
|
||||
/// > Todo: This is experimental. Changing the font size might break this.
|
||||
pub fn y_axis_width(mut self, digits: usize) -> Self {
|
||||
if let Some(main) = self.y_axes.first_mut() {
|
||||
main.digits = digits;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set custom configuration for X-axis
|
||||
///
|
||||
/// More than one axis may be specified. The first specified axis is considered the main axis.
|
||||
pub fn custom_x_axes(mut self, hints: Vec<AxisHints>) -> Self {
|
||||
self.x_axes = hints;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set custom configuration for left Y-axis
|
||||
///
|
||||
/// More than one axis may be specified. The first specified axis is considered the main axis.
|
||||
pub fn custom_y_axes(mut self, hints: Vec<AxisHints>) -> Self {
|
||||
self.y_axes = hints;
|
||||
self
|
||||
}
|
||||
|
||||
/// Interact with and add items to the plot and finally draw it.
|
||||
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> {
|
||||
self.show_dyn(ui, Box::new(build_fn))
|
||||
@@ -595,8 +675,7 @@ impl Plot {
|
||||
) -> PlotResponse<R> {
|
||||
let Self {
|
||||
id_source,
|
||||
center_x_axis,
|
||||
center_y_axis,
|
||||
center_axis,
|
||||
allow_zoom,
|
||||
allow_drag,
|
||||
allow_scroll,
|
||||
@@ -615,11 +694,13 @@ impl Plot {
|
||||
mut show_y,
|
||||
label_formatter,
|
||||
coordinates_formatter,
|
||||
axis_formatters,
|
||||
x_axes,
|
||||
y_axes,
|
||||
legend_config,
|
||||
reset,
|
||||
show_background,
|
||||
show_axes,
|
||||
show_grid,
|
||||
linked_axes,
|
||||
linked_cursors,
|
||||
|
||||
@@ -628,7 +709,9 @@ impl Plot {
|
||||
sharp_grid_lines,
|
||||
} = self;
|
||||
|
||||
// Determine the size of the plot in the UI
|
||||
// Determine position of widget.
|
||||
let pos = ui.available_rect_before_wrap().min;
|
||||
// Determine size of widget.
|
||||
let size = {
|
||||
let width = width
|
||||
.unwrap_or_else(|| {
|
||||
@@ -651,9 +734,79 @@ impl Plot {
|
||||
.at_least(min_size.y);
|
||||
vec2(width, height)
|
||||
};
|
||||
// Determine complete rect of widget.
|
||||
let complete_rect = Rect {
|
||||
min: pos,
|
||||
max: pos + size,
|
||||
};
|
||||
// Next we want to create this layout.
|
||||
// Incides are only examples.
|
||||
//
|
||||
// left right
|
||||
// +---+---------x----------+ +
|
||||
// | | X-axis 3 |
|
||||
// | +--------------------+ top
|
||||
// | | X-axis 2 |
|
||||
// +-+-+--------------------+-+-+
|
||||
// |y|y| |y|y|
|
||||
// |-|-| |-|-|
|
||||
// |A|A| |A|A|
|
||||
// y|x|x| Plot Window |x|x|
|
||||
// |i|i| |i|i|
|
||||
// |s|s| |s|s|
|
||||
// |1|0| |2|3|
|
||||
// +-+-+--------------------+-+-+
|
||||
// | X-axis 0 | |
|
||||
// +--------------------+ | bottom
|
||||
// | X-axis 1 | |
|
||||
// + +--------------------+---+
|
||||
//
|
||||
|
||||
// Allocate the space.
|
||||
let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
|
||||
let mut plot_rect: Rect = {
|
||||
// Calcuclate the space needed for each axis labels.
|
||||
let mut margin = Margin::ZERO;
|
||||
if show_axes.x {
|
||||
for cfg in &x_axes {
|
||||
match cfg.placement {
|
||||
axis::Placement::LeftBottom => {
|
||||
margin.bottom += cfg.thickness(Axis::X);
|
||||
}
|
||||
axis::Placement::RightTop => {
|
||||
margin.top += cfg.thickness(Axis::X);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if show_axes.y {
|
||||
for cfg in &y_axes {
|
||||
match cfg.placement {
|
||||
axis::Placement::LeftBottom => {
|
||||
margin.left += cfg.thickness(Axis::Y);
|
||||
}
|
||||
axis::Placement::RightTop => {
|
||||
margin.right += cfg.thickness(Axis::Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// determine plot rectangle
|
||||
margin.shrink_rect(complete_rect)
|
||||
};
|
||||
|
||||
let [mut x_axis_widgets, mut y_axis_widgets] =
|
||||
axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]);
|
||||
|
||||
// If too little space, remove axis widgets
|
||||
if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 {
|
||||
y_axis_widgets.clear();
|
||||
x_axis_widgets.clear();
|
||||
plot_rect = complete_rect;
|
||||
}
|
||||
|
||||
// Allocate the plot window.
|
||||
let response = ui.allocate_rect(plot_rect, Sense::drag());
|
||||
let rect = plot_rect;
|
||||
|
||||
// Load or initialize the memory.
|
||||
let plot_id = ui.make_persistent_id(id_source);
|
||||
@@ -677,8 +830,8 @@ impl Plot {
|
||||
last_plot_transform: PlotTransform::new(
|
||||
rect,
|
||||
min_auto_bounds,
|
||||
center_x_axis,
|
||||
center_y_axis,
|
||||
center_axis.x,
|
||||
center_axis.y,
|
||||
),
|
||||
last_click_pos_for_zoom: None,
|
||||
});
|
||||
@@ -839,7 +992,7 @@ impl Plot {
|
||||
}
|
||||
}
|
||||
|
||||
let mut transform = PlotTransform::new(rect, bounds, center_x_axis, center_y_axis);
|
||||
let mut transform = PlotTransform::new(rect, bounds, center_axis.x, center_axis.y);
|
||||
|
||||
// Enforce aspect ratio
|
||||
if let Some(data_aspect) = data_aspect {
|
||||
@@ -864,7 +1017,7 @@ impl Plot {
|
||||
delta.y = 0.0;
|
||||
}
|
||||
transform.translate_bounds(delta);
|
||||
bounds_modified = true.into();
|
||||
bounds_modified = allow_drag;
|
||||
}
|
||||
|
||||
// Zooming
|
||||
@@ -935,7 +1088,7 @@ impl Plot {
|
||||
}
|
||||
if zoom_factor != Vec2::splat(1.0) {
|
||||
transform.zoom(zoom_factor, hover_pos);
|
||||
bounds_modified = true.into();
|
||||
bounds_modified = allow_zoom;
|
||||
}
|
||||
}
|
||||
if allow_scroll {
|
||||
@@ -947,6 +1100,39 @@ impl Plot {
|
||||
}
|
||||
}
|
||||
|
||||
// --- transform initialized
|
||||
|
||||
// Add legend widgets to plot
|
||||
let bounds = transform.bounds();
|
||||
let x_axis_range = bounds.range_x();
|
||||
let x_steps = Arc::new({
|
||||
let input = GridInput {
|
||||
bounds: (bounds.min[0], bounds.max[0]),
|
||||
base_step_size: transform.dvalue_dpos()[0] * MIN_LINE_SPACING_IN_POINTS * 2.0,
|
||||
};
|
||||
(grid_spacers[0])(input)
|
||||
});
|
||||
let y_axis_range = bounds.range_y();
|
||||
let y_steps = Arc::new({
|
||||
let input = GridInput {
|
||||
bounds: (bounds.min[1], bounds.max[1]),
|
||||
base_step_size: transform.dvalue_dpos()[1] * MIN_LINE_SPACING_IN_POINTS * 2.0,
|
||||
};
|
||||
(grid_spacers[1])(input)
|
||||
});
|
||||
for mut widget in x_axis_widgets {
|
||||
widget.range = x_axis_range.clone();
|
||||
widget.transform = Some(transform);
|
||||
widget.steps = x_steps.clone();
|
||||
widget.ui(ui, Axis::X);
|
||||
}
|
||||
for mut widget in y_axis_widgets {
|
||||
widget.range = y_axis_range.clone();
|
||||
widget.transform = Some(transform);
|
||||
widget.steps = y_steps.clone();
|
||||
widget.ui(ui, Axis::Y);
|
||||
}
|
||||
|
||||
// Initialize values from functions.
|
||||
for item in &mut items {
|
||||
item.initialize(transform.bounds().range_x());
|
||||
@@ -958,16 +1144,16 @@ impl Plot {
|
||||
show_y,
|
||||
label_formatter,
|
||||
coordinates_formatter,
|
||||
axis_formatters,
|
||||
show_axes,
|
||||
show_grid,
|
||||
transform,
|
||||
draw_cursor_x: linked_cursors.as_ref().map_or(false, |(_, group)| group.x),
|
||||
draw_cursor_y: linked_cursors.as_ref().map_or(false, |(_, group)| group.y),
|
||||
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x),
|
||||
draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y),
|
||||
draw_cursors,
|
||||
grid_spacers,
|
||||
sharp_grid_lines,
|
||||
clamp_grid,
|
||||
};
|
||||
|
||||
let plot_cursors = prepared.ui(ui, &response);
|
||||
|
||||
if let Some(boxed_zoom_rect) = boxed_zoom_rect {
|
||||
@@ -1022,7 +1208,7 @@ impl Plot {
|
||||
} else {
|
||||
response
|
||||
};
|
||||
|
||||
ui.advance_cursor_after_rect(complete_rect);
|
||||
PlotResponse {
|
||||
inner,
|
||||
response,
|
||||
@@ -1031,6 +1217,79 @@ impl Plot {
|
||||
}
|
||||
}
|
||||
|
||||
fn axis_widgets(
|
||||
show_axes: AxisBools,
|
||||
plot_rect: Rect,
|
||||
[x_axes, y_axes]: [&[AxisHints]; 2],
|
||||
) -> [Vec<AxisWidget>; 2] {
|
||||
let mut x_axis_widgets = Vec::<AxisWidget>::new();
|
||||
let mut y_axis_widgets = Vec::<AxisWidget>::new();
|
||||
|
||||
// Widget count per border of plot in order left, top, right, bottom
|
||||
struct NumWidgets {
|
||||
left: usize,
|
||||
top: usize,
|
||||
right: usize,
|
||||
bottom: usize,
|
||||
}
|
||||
let mut num_widgets = NumWidgets {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
if show_axes.x {
|
||||
for cfg in x_axes {
|
||||
let size_y = Vec2::new(0.0, cfg.thickness(Axis::X));
|
||||
let rect = match cfg.placement {
|
||||
axis::Placement::LeftBottom => {
|
||||
let off = num_widgets.bottom as f32;
|
||||
num_widgets.bottom += 1;
|
||||
Rect {
|
||||
min: plot_rect.left_bottom() + size_y * off,
|
||||
max: plot_rect.right_bottom() + size_y * (off + 1.0),
|
||||
}
|
||||
}
|
||||
axis::Placement::RightTop => {
|
||||
let off = num_widgets.top as f32;
|
||||
num_widgets.top += 1;
|
||||
Rect {
|
||||
min: plot_rect.left_top() - size_y * (off + 1.0),
|
||||
max: plot_rect.right_top() - size_y * off,
|
||||
}
|
||||
}
|
||||
};
|
||||
x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
|
||||
}
|
||||
}
|
||||
if show_axes.y {
|
||||
for cfg in y_axes {
|
||||
let size_x = Vec2::new(cfg.thickness(Axis::Y), 0.0);
|
||||
let rect = match cfg.placement {
|
||||
axis::Placement::LeftBottom => {
|
||||
let off = num_widgets.left as f32;
|
||||
num_widgets.left += 1;
|
||||
Rect {
|
||||
min: plot_rect.left_top() - size_x * (off + 1.0),
|
||||
max: plot_rect.left_bottom() - size_x * off,
|
||||
}
|
||||
}
|
||||
axis::Placement::RightTop => {
|
||||
let off = num_widgets.right as f32;
|
||||
num_widgets.right += 1;
|
||||
Rect {
|
||||
min: plot_rect.right_top() + size_x * off,
|
||||
max: plot_rect.right_bottom() + size_x * (off + 1.0),
|
||||
}
|
||||
}
|
||||
};
|
||||
y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
|
||||
}
|
||||
}
|
||||
|
||||
[x_axis_widgets, y_axis_widgets]
|
||||
}
|
||||
|
||||
/// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply
|
||||
/// them at the right time, as other modifications need to happen first.
|
||||
enum BoundsModification {
|
||||
@@ -1081,17 +1340,25 @@ impl PlotUi {
|
||||
.push(BoundsModification::Translate(delta_pos));
|
||||
}
|
||||
|
||||
/// Can be used to check if the plot was hovered or clicked.
|
||||
pub fn response(&self) -> &Response {
|
||||
&self.response
|
||||
}
|
||||
|
||||
/// Returns `true` if the plot area is currently hovered.
|
||||
#[deprecated = "Use plot_ui.response().hovered()"]
|
||||
pub fn plot_hovered(&self) -> bool {
|
||||
self.response.hovered()
|
||||
}
|
||||
|
||||
/// Returns `true` if the plot was clicked by the primary button.
|
||||
#[deprecated = "Use plot_ui.response().clicked()"]
|
||||
pub fn plot_clicked(&self) -> bool {
|
||||
self.response.clicked()
|
||||
}
|
||||
|
||||
/// Returns `true` if the plot was clicked by the secondary button.
|
||||
#[deprecated = "Use plot_ui.response().secondary_clicked()"]
|
||||
pub fn plot_secondary_clicked(&self) -> bool {
|
||||
self.response.secondary_clicked()
|
||||
}
|
||||
@@ -1258,6 +1525,7 @@ pub struct GridInput {
|
||||
}
|
||||
|
||||
/// One mark (horizontal or vertical line) in the background grid of a plot.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct GridMark {
|
||||
/// X or Y value in the plot.
|
||||
pub value: f64,
|
||||
@@ -1319,14 +1587,14 @@ struct PreparedPlot {
|
||||
show_y: bool,
|
||||
label_formatter: LabelFormatter,
|
||||
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
||||
axis_formatters: [AxisFormatter; 2],
|
||||
show_axes: [bool; 2],
|
||||
// axis_formatters: [AxisFormatter; 2],
|
||||
transform: PlotTransform,
|
||||
show_grid: AxisBools,
|
||||
grid_spacers: [GridSpacer; 2],
|
||||
draw_cursor_x: bool,
|
||||
draw_cursor_y: bool,
|
||||
draw_cursors: Vec<Cursor>,
|
||||
|
||||
grid_spacers: [GridSpacer; 2],
|
||||
sharp_grid_lines: bool,
|
||||
clamp_grid: bool,
|
||||
}
|
||||
@@ -1335,16 +1603,11 @@ impl PreparedPlot {
|
||||
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
|
||||
let mut axes_shapes = Vec::new();
|
||||
|
||||
for d in 0..2 {
|
||||
if self.show_axes[d] {
|
||||
self.paint_axis(
|
||||
ui,
|
||||
d,
|
||||
self.show_axes[1 - d],
|
||||
&mut axes_shapes,
|
||||
self.sharp_grid_lines,
|
||||
);
|
||||
}
|
||||
if self.show_grid.x {
|
||||
self.paint_grid(ui, &mut axes_shapes, Axis::X);
|
||||
}
|
||||
if self.show_grid.y {
|
||||
self.paint_grid(ui, &mut axes_shapes, Axis::Y);
|
||||
}
|
||||
|
||||
// Sort the axes by strength so that those with higher strength are drawn in front.
|
||||
@@ -1421,41 +1684,27 @@ impl PreparedPlot {
|
||||
cursors
|
||||
}
|
||||
|
||||
fn paint_axis(
|
||||
&self,
|
||||
ui: &Ui,
|
||||
axis: usize,
|
||||
other_axis_shown: bool,
|
||||
shapes: &mut Vec<(Shape, f32)>,
|
||||
sharp_grid_lines: bool,
|
||||
) {
|
||||
fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis) {
|
||||
#![allow(clippy::collapsible_else_if)]
|
||||
|
||||
let Self {
|
||||
transform,
|
||||
axis_formatters,
|
||||
// axis_formatters,
|
||||
grid_spacers,
|
||||
clamp_grid,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let bounds = transform.bounds();
|
||||
let axis_range = match axis {
|
||||
0 => bounds.range_x(),
|
||||
1 => bounds.range_y(),
|
||||
_ => panic!("Axis {} does not exist.", axis),
|
||||
};
|
||||
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let iaxis = usize::from(axis);
|
||||
|
||||
// Where on the cross-dimension to show the label values
|
||||
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
|
||||
let bounds = transform.bounds();
|
||||
let value_cross = 0.0_f64.clamp(bounds.min[1 - iaxis], bounds.max[1 - iaxis]);
|
||||
|
||||
let input = GridInput {
|
||||
bounds: (bounds.min[axis], bounds.max[axis]),
|
||||
base_step_size: transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS,
|
||||
bounds: (bounds.min[iaxis], bounds.max[iaxis]),
|
||||
base_step_size: transform.dvalue_dpos()[iaxis] * MIN_LINE_SPACING_IN_POINTS,
|
||||
};
|
||||
let steps = (grid_spacers[axis])(input);
|
||||
let steps = (grid_spacers[iaxis])(input);
|
||||
|
||||
let clamp_range = clamp_grid.then(|| {
|
||||
let mut tight_bounds = PlotBounds::NOTHING;
|
||||
@@ -1471,25 +1720,27 @@ impl PreparedPlot {
|
||||
let value_main = step.value;
|
||||
|
||||
if let Some(clamp_range) = clamp_range {
|
||||
if axis == 0 {
|
||||
if !clamp_range.range_x().contains(&value_main) {
|
||||
continue;
|
||||
};
|
||||
} else {
|
||||
if !clamp_range.range_y().contains(&value_main) {
|
||||
continue;
|
||||
};
|
||||
match axis {
|
||||
Axis::X => {
|
||||
if !clamp_range.range_x().contains(&value_main) {
|
||||
continue;
|
||||
};
|
||||
}
|
||||
Axis::Y => {
|
||||
if !clamp_range.range_y().contains(&value_main) {
|
||||
continue;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let value = if axis == 0 {
|
||||
PlotPoint::new(value_main, value_cross)
|
||||
} else {
|
||||
PlotPoint::new(value_cross, value_main)
|
||||
let value = match axis {
|
||||
Axis::X => PlotPoint::new(value_main, value_cross),
|
||||
Axis::Y => PlotPoint::new(value_cross, value_main),
|
||||
};
|
||||
|
||||
let pos_in_gui = transform.position_from_point(&value);
|
||||
let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32;
|
||||
let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32;
|
||||
|
||||
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
|
||||
let line_strength = remap_clamp(
|
||||
@@ -1498,24 +1749,27 @@ impl PreparedPlot {
|
||||
0.0..=1.0,
|
||||
);
|
||||
|
||||
let line_color = color_from_contrast(ui, line_strength);
|
||||
let line_color = color_from_strength(ui, line_strength);
|
||||
|
||||
let mut p0 = pos_in_gui;
|
||||
let mut p1 = pos_in_gui;
|
||||
p0[1 - axis] = transform.frame().min[1 - axis];
|
||||
p1[1 - axis] = transform.frame().max[1 - axis];
|
||||
p0[1 - iaxis] = transform.frame().min[1 - iaxis];
|
||||
p1[1 - iaxis] = transform.frame().max[1 - iaxis];
|
||||
|
||||
if let Some(clamp_range) = clamp_range {
|
||||
if axis == 0 {
|
||||
p0.y = transform.position_from_point_y(clamp_range.min[1]);
|
||||
p1.y = transform.position_from_point_y(clamp_range.max[1]);
|
||||
} else {
|
||||
p0.x = transform.position_from_point_x(clamp_range.min[0]);
|
||||
p1.x = transform.position_from_point_x(clamp_range.max[0]);
|
||||
match axis {
|
||||
Axis::X => {
|
||||
p0.y = transform.position_from_point_y(clamp_range.min[1]);
|
||||
p1.y = transform.position_from_point_y(clamp_range.max[1]);
|
||||
}
|
||||
Axis::Y => {
|
||||
p0.x = transform.position_from_point_x(clamp_range.min[0]);
|
||||
p1.x = transform.position_from_point_x(clamp_range.max[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sharp_grid_lines {
|
||||
if self.sharp_grid_lines {
|
||||
// Round to avoid aliasing
|
||||
p0 = ui.ctx().round_pos_to_pixels(p0);
|
||||
p1 = ui.ctx().round_pos_to_pixels(p1);
|
||||
@@ -1526,47 +1780,6 @@ impl PreparedPlot {
|
||||
line_strength,
|
||||
));
|
||||
}
|
||||
|
||||
const MIN_TEXT_SPACING: f32 = 40.0;
|
||||
if spacing_in_points > MIN_TEXT_SPACING {
|
||||
let text_strength =
|
||||
remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0);
|
||||
let color = color_from_contrast(ui, text_strength);
|
||||
|
||||
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
|
||||
formatter(value_main, &axis_range)
|
||||
} else {
|
||||
emath::round_to_decimals(value_main, 5).to_string() // hack
|
||||
};
|
||||
|
||||
// Skip origin label for y-axis if x-axis is already showing it (otherwise displayed twice)
|
||||
let skip_origin_y = axis == 1 && other_axis_shown && value_main == 0.0;
|
||||
|
||||
// Custom formatters can return empty string to signal "no label at this resolution"
|
||||
if !text.is_empty() && !skip_origin_y {
|
||||
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
|
||||
|
||||
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
||||
|
||||
// Make sure we see the labels, even if the axis is off-screen:
|
||||
text_pos[1 - axis] = text_pos[1 - axis]
|
||||
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
||||
.at_least(transform.frame().min[1 - axis] + 1.0);
|
||||
|
||||
shapes.push((Shape::galley(text_pos, galley), text_strength));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 {
|
||||
let bg = ui.visuals().extreme_bg_color;
|
||||
let fg = ui.visuals().widgets.open.fg_stroke.color;
|
||||
let mix = 0.5 * contrast.sqrt();
|
||||
Color32::from_rgb(
|
||||
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
|
||||
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
|
||||
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1666,9 +1879,21 @@ pub fn format_number(number: f64, num_decimals: usize) -> String {
|
||||
let is_integral = number as i64 as f64 == number;
|
||||
if is_integral {
|
||||
// perfect integer - show it as such:
|
||||
format!("{:.0}", number)
|
||||
format!("{number:.0}")
|
||||
} else {
|
||||
// make sure we tell the user it is not an integer by always showing a decimal or two:
|
||||
format!("{:.*}", num_decimals.at_least(1), number)
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine a color from a 0-1 strength value.
|
||||
pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 {
|
||||
let bg = ui.visuals().extreme_bg_color;
|
||||
let fg = ui.visuals().widgets.open.fg_stroke.color;
|
||||
let mix = 0.5 * strength.sqrt();
|
||||
Color32::from_rgb(
|
||||
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
|
||||
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
|
||||
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,8 +77,10 @@ pub struct Slider<'a> {
|
||||
prefix: String,
|
||||
suffix: String,
|
||||
text: WidgetText,
|
||||
|
||||
/// Sets the minimal step of the widget value
|
||||
step: Option<f64>,
|
||||
|
||||
drag_value_speed: Option<f64>,
|
||||
min_decimals: usize,
|
||||
max_decimals: Option<usize>,
|
||||
@@ -524,12 +526,12 @@ impl<'a> Slider<'a> {
|
||||
}
|
||||
|
||||
/// For instance, `position` is the mouse position and `position_range` is the physical location of the slider on the screen.
|
||||
fn value_from_position(&self, position: f32, position_range: RangeInclusive<f32>) -> f64 {
|
||||
fn value_from_position(&self, position: f32, position_range: Rangef) -> f64 {
|
||||
let normalized = remap_clamp(position, position_range, 0.0..=1.0) as f64;
|
||||
value_from_normalized(normalized, self.range(), &self.spec)
|
||||
}
|
||||
|
||||
fn position_from_value(&self, value: f64, position_range: RangeInclusive<f32>) -> f32 {
|
||||
fn position_from_value(&self, value: f64, position_range: Rangef) -> f32 {
|
||||
let normalized = normalized_from_value(value, self.range(), &self.spec);
|
||||
lerp(position_range, normalized as f32)
|
||||
}
|
||||
@@ -555,11 +557,11 @@ impl<'a> Slider<'a> {
|
||||
let new_value = if self.smart_aim {
|
||||
let aim_radius = ui.input(|i| i.aim_radius());
|
||||
emath::smart_aim::best_in_range_f64(
|
||||
self.value_from_position(position - aim_radius, position_range.clone()),
|
||||
self.value_from_position(position + aim_radius, position_range.clone()),
|
||||
self.value_from_position(position - aim_radius, position_range),
|
||||
self.value_from_position(position + aim_radius, position_range),
|
||||
)
|
||||
} else {
|
||||
self.value_from_position(position, position_range.clone())
|
||||
self.value_from_position(position, position_range)
|
||||
};
|
||||
self.set_value(new_value);
|
||||
}
|
||||
@@ -594,18 +596,18 @@ impl<'a> Slider<'a> {
|
||||
|
||||
if kb_step != 0.0 {
|
||||
let prev_value = self.get_value();
|
||||
let prev_position = self.position_from_value(prev_value, position_range.clone());
|
||||
let prev_position = self.position_from_value(prev_value, position_range);
|
||||
let new_position = prev_position + kb_step;
|
||||
let new_value = match self.step {
|
||||
Some(step) => prev_value + (kb_step as f64 * step),
|
||||
None if self.smart_aim => {
|
||||
let aim_radius = ui.input(|i| i.aim_radius());
|
||||
emath::smart_aim::best_in_range_f64(
|
||||
self.value_from_position(new_position - aim_radius, position_range.clone()),
|
||||
self.value_from_position(new_position + aim_radius, position_range.clone()),
|
||||
self.value_from_position(new_position - aim_radius, position_range),
|
||||
self.value_from_position(new_position + aim_radius, position_range),
|
||||
)
|
||||
}
|
||||
_ => self.value_from_position(new_position, position_range.clone()),
|
||||
_ => self.value_from_position(new_position, position_range),
|
||||
};
|
||||
self.set_value(new_value);
|
||||
}
|
||||
@@ -686,15 +688,11 @@ impl<'a> Slider<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn position_range(&self, rect: &Rect) -> RangeInclusive<f32> {
|
||||
fn position_range(&self, rect: &Rect) -> Rangef {
|
||||
let handle_radius = self.handle_radius(rect);
|
||||
match self.orientation {
|
||||
SliderOrientation::Horizontal => {
|
||||
(rect.left() + handle_radius)..=(rect.right() - handle_radius)
|
||||
}
|
||||
SliderOrientation::Vertical => {
|
||||
(rect.bottom() - handle_radius)..=(rect.top() + handle_radius)
|
||||
}
|
||||
SliderOrientation::Horizontal => rect.x_range().shrink(handle_radius),
|
||||
SliderOrientation::Vertical => rect.y_range().shrink(handle_radius),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,7 +724,7 @@ impl<'a> Slider<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) -> Response {
|
||||
fn value_ui(&mut self, ui: &mut Ui, position_range: Rangef) -> Response {
|
||||
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
|
||||
let change = ui.input(|input| {
|
||||
input.num_presses(Key::ArrowUp) as i32 + input.num_presses(Key::ArrowRight) as i32
|
||||
@@ -740,7 +738,7 @@ impl<'a> Slider<'a> {
|
||||
step
|
||||
} else {
|
||||
self.drag_value_speed
|
||||
.unwrap_or_else(|| self.current_gradient(&position_range))
|
||||
.unwrap_or_else(|| self.current_gradient(position_range))
|
||||
};
|
||||
|
||||
let mut value = self.get_value();
|
||||
@@ -767,12 +765,11 @@ impl<'a> Slider<'a> {
|
||||
}
|
||||
|
||||
/// delta(value) / delta(points)
|
||||
fn current_gradient(&mut self, position_range: &RangeInclusive<f32>) -> f64 {
|
||||
fn current_gradient(&mut self, position_range: Rangef) -> f64 {
|
||||
// TODO(emilk): handle clamping
|
||||
let value = self.get_value();
|
||||
let value_from_pos =
|
||||
|position: f32| self.value_from_position(position, position_range.clone());
|
||||
let pos_from_value = |value: f64| self.position_from_value(value, position_range.clone());
|
||||
let value_from_pos = |position: f32| self.value_from_position(position, position_range);
|
||||
let pos_from_value = |value: f64| self.position_from_value(value, position_range);
|
||||
let left_value = value_from_pos(pos_from_value(value) - 0.5);
|
||||
let right_value = value_from_pos(pos_from_value(value) + 0.5);
|
||||
right_value - left_value
|
||||
|
||||
@@ -1138,7 +1138,7 @@ fn paint_cursor_end(
|
||||
galley: &Galley,
|
||||
cursor: &Cursor,
|
||||
) -> Rect {
|
||||
let stroke = ui.visuals().selection.stroke;
|
||||
let stroke = ui.visuals().text_cursor;
|
||||
|
||||
let mut cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
|
||||
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height); // Handle completely empty galleys
|
||||
@@ -1147,10 +1147,7 @@ fn paint_cursor_end(
|
||||
let top = cursor_pos.center_top();
|
||||
let bottom = cursor_pos.center_bottom();
|
||||
|
||||
painter.line_segment(
|
||||
[top, bottom],
|
||||
(ui.visuals().text_cursor_width, stroke.color),
|
||||
);
|
||||
painter.line_segment([top, bottom], (stroke.width, stroke.color));
|
||||
|
||||
if false {
|
||||
// Roof/floor:
|
||||
@@ -1185,7 +1182,7 @@ fn insert_text(
|
||||
if char_limit < usize::MAX {
|
||||
let mut new_string = text_to_insert;
|
||||
// Avoid subtract with overflow panic
|
||||
let cutoff = char_limit.saturating_sub(text.as_str().len());
|
||||
let cutoff = char_limit.saturating_sub(text.as_str().chars().count());
|
||||
|
||||
new_string = match new_string.char_indices().nth(cutoff) {
|
||||
None => new_string,
|
||||
|
||||
Reference in New Issue
Block a user