diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5175d8..be641726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w ### Changed 🔧 * Renamed `Ui::visible` to `Ui::is_visible`. +### Fixed 🐛 +* Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)) ## 0.16.1 - 2021-12-31 - Add back `CtxRef::begin_frame,end_frame` @@ -49,7 +51,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w ### Fixed 🐛 * Fixed `ComboBox` and other popups getting clipped to parent window ([#885](https://github.com/emilk/egui/pull/885)). -* The color picker is now better att keeping the same hue even when saturation goes to zero ([#886](https://github.com/emilk/egui/pull/886)). +* The color picker is now better at keeping the same hue even when saturation goes to zero ([#886](https://github.com/emilk/egui/pull/886)). ### Removed 🔥 * Removed `egui::math` (use `egui::emath` instead). diff --git a/Cargo.lock b/Cargo.lock index 21e50dee..d2c373df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -969,6 +969,7 @@ dependencies = [ "atomic_refcell", "bytemuck", "cint", + "criterion", "emath", "nohash-hasher", "parking_lot", diff --git a/egui-winit/src/lib.rs b/egui-winit/src/lib.rs index a3288954..1fa2d22d 100644 --- a/egui-winit/src/lib.rs +++ b/egui-winit/src/lib.rs @@ -454,12 +454,10 @@ impl State { } }; if cfg!(target_os = "macos") { - // This is still buggy in winit despite - // https://github.com/rust-windowing/winit/issues/1695 being closed - delta.x *= -1.0; + delta.x *= -1.0; // until https://github.com/rust-windowing/winit/pull/2105 is merged and released } if cfg!(target_os = "windows") { - delta.x *= -1.0; // until https://github.com/rust-windowing/winit/pull/2101 is merged + delta.x *= -1.0; // until https://github.com/rust-windowing/winit/pull/2101 is merged and released } if self.egui_input.modifiers.ctrl || self.egui_input.modifiers.command { diff --git a/egui/src/containers/panel.rs b/egui/src/containers/panel.rs index d218d99b..8bdea2a8 100644 --- a/egui/src/containers/panel.rs +++ b/egui/src/containers/panel.rs @@ -117,6 +117,14 @@ impl SidePanel { /// Can panel be resized by dragging the edge of it? /// /// Default is `true`. + /// + /// If you want your panel to be resizable you also need a widget in it that + /// takes up more space as you resize it, such as: + /// * Wrapping text ([`Ui::horizontal_wrapped`]). + /// * A [`ScrollArea`]. + /// * A [`Separator`]. + /// * A [`TextEdit`]. + /// * … pub fn resizable(mut self, resizable: bool) -> Self { self.resizable = resizable; self @@ -393,6 +401,14 @@ impl TopBottomPanel { /// Can panel be resized by dragging the edge of it? /// /// Default is `false`. + /// + /// If you want your panel to be resizable you also need a widget in it that + /// takes up more space as you resize it, such as: + /// * Wrapping text ([`Ui::horizontal_wrapped`]). + /// * A [`ScrollArea`]. + /// * A [`Separator`]. + /// * A [`TextEdit`]. + /// * … pub fn resizable(mut self, resizable: bool) -> Self { self.resizable = resizable; self diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index 6bfc942a..a79bf5df 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -376,10 +376,10 @@ impl ScrollArea { /// let text_style = egui::TextStyle::Body; /// let row_height = ui.fonts()[text_style].row_height(); /// // let row_height = ui.spacing().interact_size.y; // if you are adding buttons instead of labels. - /// let num_rows = 10_000; - /// egui::ScrollArea::vertical().show_rows(ui, row_height, num_rows, |ui, row_range| { + /// let total_rows = 10_000; + /// egui::ScrollArea::vertical().show_rows(ui, row_height, total_rows, |ui, row_range| { /// for row in row_range { - /// let text = format!("Row {}/{}", row + 1, num_rows); + /// let text = format!("Row {}/{}", row + 1, total_rows); /// ui.label(text); /// } /// }); @@ -389,19 +389,19 @@ impl ScrollArea { self, ui: &mut Ui, row_height_sans_spacing: f32, - num_rows: usize, + total_rows: usize, add_contents: impl FnOnce(&mut Ui, std::ops::Range) -> R, ) -> R { let spacing = ui.spacing().item_spacing; let row_height_with_spacing = row_height_sans_spacing + spacing.y; self.show_viewport(ui, |ui, viewport| { - ui.set_height((row_height_with_spacing * num_rows as f32 - spacing.y).at_least(0.0)); + ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0)); let min_row = (viewport.min.y / row_height_with_spacing) .floor() .at_least(0.0) as usize; let max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1; - let max_row = max_row.at_most(num_rows); + let max_row = max_row.at_most(total_rows); let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing; let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing; diff --git a/egui/src/context.rs b/egui/src/context.rs index 9e28348d..ad5c8482 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -100,7 +100,7 @@ impl CtxRef { /// This will modify the internal reference to point to a new generation of [`Context`]. /// Any old clones of this [`CtxRef`] will refer to the old [`Context`], which will not get new input. /// - /// You can alternatively run [`Self::begin_frame`] and [`Self::end_frame`]. + /// You can alternatively run [`Self::begin_frame`] and [`Context::end_frame`]. /// /// ``` rust /// // One egui context that you keep reusing: diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index 1f7c4bf2..27a07a0c 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -174,6 +174,10 @@ pub enum Event { PointerGone, /// How many points (logical pixels) the user scrolled. + /// + /// The direction of the vector indicates how to move the _content_ that is being viewed. + /// So if you get positive values, the content being viewed should move to the right and down, + /// revealing new things to the left and up. Scroll(Vec2), /// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture). diff --git a/egui/src/lib.rs b/egui/src/lib.rs index 76ea7532..eb4c8d70 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -214,7 +214,7 @@ //! when you release the panel/window shrinks again. //! This is an artifact of immediate mode, and here are some alternatives on how to avoid it: //! -//! 1. Turn of resizing with [`Window::resizable`], [`SidePanel::resizable`], [`TopBottomPanel::resizable`]. +//! 1. Turn off resizing with [`Window::resizable`], [`SidePanel::resizable`], [`TopBottomPanel::resizable`]. //! 2. Wrap your panel contents in a [`ScrollArea`], or use [`Window::vscroll`] and [`Window::hscroll`]. //! 3. Use a justified layout: //! diff --git a/egui/src/menu.rs b/egui/src/menu.rs index 986d695e..e8812d18 100644 --- a/egui/src/menu.rs +++ b/egui/src/menu.rs @@ -16,9 +16,8 @@ //! ``` use super::{ - style::{Spacing, WidgetVisuals}, - Align, CtxRef, Id, InnerResponse, PointerState, Pos2, Rect, Response, Sense, Style, TextStyle, - Ui, Vec2, + style::WidgetVisuals, Align, CtxRef, Id, InnerResponse, PointerState, Pos2, Rect, Response, + Sense, TextStyle, Ui, Vec2, }; use crate::{widgets::*, *}; use epaint::{mutex::RwLock, Stroke}; @@ -120,7 +119,6 @@ pub(crate) fn menu_ui<'c, R>( ctx: &CtxRef, menu_id: impl std::hash::Hash, menu_state_arc: &Arc>, - mut style: Style, add_contents: impl FnOnce(&mut Ui) -> R + 'c, ) -> InnerResponse { let pos = { @@ -128,29 +126,34 @@ pub(crate) fn menu_ui<'c, R>( menu_state.entry_count = 0; menu_state.rect.min }; - // style.visuals.widgets.active.bg_fill = Color32::TRANSPARENT; - style.visuals.widgets.active.bg_stroke = Stroke::none(); - // style.visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT; - style.visuals.widgets.hovered.bg_stroke = Stroke::none(); - style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT; - style.visuals.widgets.inactive.bg_stroke = Stroke::none(); + let area = Area::new(menu_id) .order(Order::Foreground) .fixed_pos(pos) .interactable(false) .drag_bounds(Rect::EVERYTHING); - let frame = Frame::menu(&style); let inner_response = area.show(ctx, |ui| { - frame - .show(ui, |ui| { - const DEFAULT_MENU_WIDTH: f32 = 150.0; // TODO: add to ui.spacing - ui.set_max_width(DEFAULT_MENU_WIDTH); - ui.set_style(style); - ui.set_menu_state(Some(menu_state_arc.clone())); - ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents) - .inner - }) - .inner + ui.scope(|ui| { + let style = ui.style_mut(); + style.spacing.item_spacing = Vec2::ZERO; + style.spacing.button_padding = crate::vec2(2.0, 0.0); + + style.visuals.widgets.active.bg_stroke = Stroke::none(); + style.visuals.widgets.hovered.bg_stroke = Stroke::none(); + style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT; + style.visuals.widgets.inactive.bg_stroke = Stroke::none(); + + Frame::menu(style) + .show(ui, |ui| { + const DEFAULT_MENU_WIDTH: f32 = 150.0; // TODO: add to ui.spacing + ui.set_max_width(DEFAULT_MENU_WIDTH); + ui.set_menu_state(Some(menu_state_arc.clone())); + ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents) + .inner + }) + .inner + }) + .inner }); menu_state_arc.write().rect = inner_response.response.rect; inner_response @@ -522,15 +525,7 @@ impl MenuState { id: Id, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { - let style = Style { - spacing: Spacing { - item_spacing: Vec2::ZERO, - button_padding: crate::vec2(2.0, 0.0), - ..Default::default() - }, - ..Default::default() - }; - crate::menu::menu_ui(ctx, id, menu_state, style, add_contents) + crate::menu::menu_ui(ctx, id, menu_state, add_contents) } fn show_submenu( &mut self, diff --git a/egui/src/widgets/progress_bar.rs b/egui/src/widgets/progress_bar.rs index 9f277aa3..9c74f473 100644 --- a/egui/src/widgets/progress_bar.rs +++ b/egui/src/widgets/progress_bar.rs @@ -6,6 +6,9 @@ enum ProgressBarText { } /// A simple progress bar. +/// +/// See also: [`crate::Spinner`]. +#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] pub struct ProgressBar { progress: f32, desired_width: Option, @@ -62,10 +65,6 @@ impl Widget for ProgressBar { let animate = animate && progress < 1.0; - if animate { - ui.ctx().request_repaint(); - } - let desired_width = desired_width.unwrap_or_else(|| ui.available_size_before_wrap().x.at_least(96.0)); let height = ui.spacing().interact_size.y; @@ -73,6 +72,10 @@ impl Widget for ProgressBar { ui.allocate_exact_size(vec2(desired_width, height), Sense::hover()); if ui.is_rect_visible(response.rect) { + if animate { + ui.ctx().request_repaint(); + } + let visuals = ui.style().visuals.clone(); let corner_radius = outer_rect.height() / 2.0; ui.painter().rect( diff --git a/egui/src/widgets/spinner.rs b/egui/src/widgets/spinner.rs index 24484abf..1fbc19ca 100644 --- a/egui/src/widgets/spinner.rs +++ b/egui/src/widgets/spinner.rs @@ -3,6 +3,8 @@ use epaint::{emath::lerp, vec2, Pos2, Shape, Stroke}; use crate::{Response, Sense, Ui, Widget}; /// A spinner widget used to indicate loading. +/// +/// See also: [`crate::ProgressBar`]. #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[derive(Default)] pub struct Spinner { diff --git a/egui/src/widgets/text_edit/builder.rs b/egui/src/widgets/text_edit/builder.rs index 8c6812e1..95d911ab 100644 --- a/egui/src/widgets/text_edit/builder.rs +++ b/egui/src/widgets/text_edit/builder.rs @@ -44,6 +44,8 @@ use super::{CCursorRange, CursorRange, TextEditOutput, TextEditState}; /// } /// ``` /// +/// ## Advanced usage +/// See [`TextEdit::show`]. #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] pub struct TextEdit<'t> { text: &'t mut dyn TextBuffer, @@ -242,6 +244,20 @@ impl<'t> Widget for TextEdit<'t> { impl<'t> TextEdit<'t> { /// Show the [`TextEdit`], returning a rich [`TextEditOutput`]. + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let mut my_string = String::new(); + /// let output = egui::TextEdit::singleline(&mut my_string).show(ui); + /// if let Some(text_cursor_range) = output.cursor_range { + /// use egui::TextBuffer as _; + /// let selected_chars = text_cursor_range.as_sorted_char_range(); + /// let selected_text = my_string.char_range(selected_chars); + /// ui.label("Selected text: "); + /// ui.monospace(selected_text); + /// } + /// # }); + /// ``` pub fn show(self, ui: &mut Ui) -> TextEditOutput { let is_mutable = self.text.is_mutable(); let frame = self.frame; @@ -386,7 +402,8 @@ impl<'t> TextEdit<'t> { Sense::hover() }; let mut response = ui.interact(rect, id, sense); - let painter = ui.painter_at(rect); + let text_clip_rect = rect; + let painter = ui.painter_at(text_clip_rect); if interactive { if let Some(pointer_pos) = ui.input().pointer.interact_pos() { @@ -593,6 +610,8 @@ impl<'t> TextEdit<'t> { TextEditOutput { response, galley, + text_draw_pos, + text_clip_rect, state, cursor_range, } @@ -806,7 +825,7 @@ fn paint_cursor_selection( // We paint the cursor selection on top of the text, so make it transparent: let color = ui.visuals().selection.bg_fill.linear_multiply(0.5); - let [min, max] = cursor_range.sorted(); + let [min, max] = cursor_range.sorted_cursors(); let min = min.rcursor; let max = max.rcursor; @@ -875,7 +894,7 @@ fn paint_cursor_end( // ---------------------------------------------------------------------------- fn selected_str<'s>(text: &'s dyn TextBuffer, cursor_range: &CursorRange) -> &'s str { - let [min, max] = cursor_range.sorted(); + let [min, max] = cursor_range.sorted_cursors(); text.char_range(min.ccursor.index..max.ccursor.index) } @@ -886,7 +905,7 @@ fn insert_text(ccursor: &mut CCursor, text: &mut dyn TextBuffer, text_to_insert: // ---------------------------------------------------------------------------- fn delete_selected(text: &mut dyn TextBuffer, cursor_range: &CursorRange) -> CCursor { - let [min, max] = cursor_range.sorted(); + let [min, max] = cursor_range.sorted_cursors(); delete_selected_ccursor_range(text, [min.ccursor, max.ccursor]) } @@ -927,7 +946,7 @@ fn delete_paragraph_before_cursor( galley: &Galley, cursor_range: &CursorRange, ) -> CCursor { - let [min, max] = cursor_range.sorted(); + let [min, max] = cursor_range.sorted_cursors(); let min = galley.from_pcursor(PCursor { paragraph: min.pcursor.paragraph, offset: 0, @@ -945,7 +964,7 @@ fn delete_paragraph_after_cursor( galley: &Galley, cursor_range: &CursorRange, ) -> CCursor { - let [min, max] = cursor_range.sorted(); + let [min, max] = cursor_range.sorted_cursors(); let max = galley.from_pcursor(PCursor { paragraph: max.pcursor.paragraph, offset: usize::MAX, // end of paragraph @@ -984,7 +1003,7 @@ fn on_key_press( }; Some(CCursorRange::one(ccursor)) } - Key::Delete if !(cfg!(target_os = "windows") && modifiers.shift) => { + Key::Delete if !modifiers.shift || !cfg!(target_os = "windows") => { let ccursor = if modifiers.mac_cmd { delete_paragraph_after_cursor(text, galley, cursor_range) } else if let Some(cursor) = cursor_range.single() { @@ -1031,9 +1050,9 @@ fn on_key_press( Key::ArrowLeft | Key::ArrowRight if modifiers.is_none() && !cursor_range.is_empty() => { if key == Key::ArrowLeft { - *cursor_range = CursorRange::one(cursor_range.sorted()[0]); + *cursor_range = CursorRange::one(cursor_range.sorted_cursors()[0]); } else { - *cursor_range = CursorRange::one(cursor_range.sorted()[1]); + *cursor_range = CursorRange::one(cursor_range.sorted_cursors()[1]); } None } diff --git a/egui/src/widgets/text_edit/cursor_range.rs b/egui/src/widgets/text_edit/cursor_range.rs index 938de28a..360260b8 100644 --- a/egui/src/widgets/text_edit/cursor_range.rs +++ b/egui/src/widgets/text_edit/cursor_range.rs @@ -37,6 +37,15 @@ impl CursorRange { } } + /// The range of selected character indices. + pub fn as_sorted_char_range(&self) -> std::ops::Range { + let [start, end] = self.sorted_cursors(); + std::ops::Range { + start: start.ccursor.index, + end: end.ccursor.index, + } + } + /// True if the selected range contains no characters. pub fn is_empty(&self) -> bool { self.primary.ccursor == self.secondary.ccursor @@ -58,8 +67,19 @@ impl CursorRange { (p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row) } + pub fn sorted(self) -> Self { + if self.is_sorted() { + self + } else { + Self { + primary: self.secondary, + secondary: self.primary, + } + } + } + /// returns the two ends ordered - pub fn sorted(&self) -> [Cursor; 2] { + pub fn sorted_cursors(&self) -> [Cursor; 2] { if self.is_sorted() { [self.primary, self.secondary] } else { diff --git a/egui/src/widgets/text_edit/output.rs b/egui/src/widgets/text_edit/output.rs index fecc4c71..0464d646 100644 --- a/egui/src/widgets/text_edit/output.rs +++ b/egui/src/widgets/text_edit/output.rs @@ -8,9 +8,17 @@ pub struct TextEditOutput { /// How the text was displayed. pub galley: Arc, - /// The state we stored after the run/ + /// Where the text in [`Self::galley`] ended up on the screen. + pub text_draw_pos: crate::Pos2, + + /// The text was clipped to this rectangle when painted. + pub text_clip_rect: crate::Rect, + + /// The state we stored after the run. pub state: super::TextEditState, /// Where the text cursor is. pub cursor_range: Option, } + +// TODO: add `output.paint` and `output.store` and split out that code from `TextEdit::show`. diff --git a/egui_demo_lib/src/apps/demo/demo_app_windows.rs b/egui_demo_lib/src/apps/demo/demo_app_windows.rs index 19125c67..c59ca9c2 100644 --- a/egui_demo_lib/src/apps/demo/demo_app_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_app_windows.rs @@ -30,6 +30,7 @@ impl Default for Demos { Box::new(super::scrolling::Scrolling::default()), Box::new(super::sliders::Sliders::default()), Box::new(super::table_demo::TableDemo::default()), + Box::new(super::text_edit::TextEdit::default()), Box::new(super::widget_gallery::WidgetGallery::default()), Box::new(super::window_options::WindowOptions::default()), Box::new(super::tests::WindowResizeTest::default()), diff --git a/egui_demo_lib/src/apps/demo/mod.rs b/egui_demo_lib/src/apps/demo/mod.rs index 6dc85c14..ee9c6f5e 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -23,6 +23,7 @@ pub mod scrolling; pub mod sliders; pub mod table_demo; pub mod tests; +pub mod text_edit; pub mod toggle_switch; pub mod widget_gallery; pub mod window_options; diff --git a/egui_demo_lib/src/apps/demo/text_edit.rs b/egui_demo_lib/src/apps/demo/text_edit.rs new file mode 100644 index 00000000..1d2294de --- /dev/null +++ b/egui_demo_lib/src/apps/demo/text_edit.rs @@ -0,0 +1,83 @@ +/// Showcase [`TextEdit`]. +#[derive(PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct TextEdit { + pub text: String, +} + +impl Default for TextEdit { + fn default() -> Self { + Self { + text: "Edit this text".to_owned(), + } + } +} + +impl super::Demo for TextEdit { + fn name(&self) -> &'static str { + "🖹 TextEdit" + } + + fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .show(ctx, |ui| { + use super::View as _; + self.ui(ui); + }); + } +} + +impl super::View for TextEdit { + fn ui(&mut self, ui: &mut egui::Ui) { + let Self { text } = self; + + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("Advanced usage of "); + ui.code("TextEdit"); + ui.label("."); + }); + + let output = egui::TextEdit::multiline(text) + .hint_text("Type something!") + .show(ui); + + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("Selected text: "); + if let Some(text_cursor_range) = output.cursor_range { + use egui::TextBuffer as _; + let selected_chars = text_cursor_range.as_sorted_char_range(); + let selected_text = text.char_range(selected_chars); + ui.code(selected_text); + } + }); + + let anything_selected = output + .cursor_range + .map_or(false, |cursor| !cursor.is_empty()); + + ui.add_enabled( + anything_selected, + egui::Label::new("Press ctrl+T to toggle the case of selected text (cmd+T on Mac)"), + ); + if ui.input().modifiers.command_only() && ui.input().key_pressed(egui::Key::T) { + if let Some(text_cursor_range) = output.cursor_range { + use egui::TextBuffer as _; + let selected_chars = text_cursor_range.as_sorted_char_range(); + let selected_text = text.char_range(selected_chars.clone()); + let upper_case = selected_text.to_uppercase(); + let new_text = if selected_text == upper_case { + selected_text.to_lowercase() + } else { + upper_case + }; + text.delete_char_range(selected_chars.clone()); + text.insert_text(&new_text, selected_chars.start); + } + } + } +} diff --git a/egui_glow/CHANGELOG.md b/egui_glow/CHANGELOG.md index 5fa3505f..4d107dcb 100644 --- a/egui_glow/CHANGELOG.md +++ b/egui_glow/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to the `egui_glow` integration will be noted in this file. ## Unreleased - +* Added `set_texture_filter` method to `Painter` ((#1041)[https://github.com/emilk/egui/pull/1041]). ## 0.16.0 - 2021-12-29 * Made winit/glutin an optional dependency ([#868](https://github.com/emilk/egui/pull/868)). diff --git a/egui_glow/src/misc_util.rs b/egui_glow/src/misc_util.rs index 0ce12000..7c711813 100644 --- a/egui_glow/src/misc_util.rs +++ b/egui_glow/src/misc_util.rs @@ -2,10 +2,13 @@ use glow::HasContext; use std::option::Option::Some; +use crate::painter::TextureFilter; + pub(crate) fn srgbtexture2d( gl: &glow::Context, is_webgl_1: bool, srgb_support: bool, + texture_filter: TextureFilter, data: &[u8], w: usize, h: usize, @@ -20,12 +23,12 @@ pub(crate) fn srgbtexture2d( gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, - glow::LINEAR as i32, + texture_filter.glow_code() as i32, ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, - glow::LINEAR as i32, + texture_filter.glow_code() as i32, ); gl.tex_parameter_i32( glow::TEXTURE_2D, diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs index 49f35eb2..a23037a0 100644 --- a/egui_glow/src/painter.rs +++ b/egui_glow/src/painter.rs @@ -35,6 +35,8 @@ pub struct Painter { is_embedded: bool, vertex_array: crate::misc_util::VAO, srgb_support: bool, + /// The filter used for subsequent textures. + texture_filter: TextureFilter, post_process: Option, vertex_buffer: glow::Buffer, element_array_buffer: glow::Buffer, @@ -52,6 +54,27 @@ pub struct Painter { destroyed: bool, } +#[derive(Copy, Clone)] +pub enum TextureFilter { + Linear, + Nearest, +} + +impl Default for TextureFilter { + fn default() -> Self { + TextureFilter::Linear + } +} + +impl TextureFilter { + pub(crate) fn glow_code(&self) -> u32 { + match self { + TextureFilter::Linear => glow::LINEAR, + TextureFilter::Nearest => glow::NEAREST, + } + } +} + impl Painter { /// Create painter. /// @@ -190,6 +213,7 @@ impl Painter { is_embedded: matches!(shader_version, ShaderVersion::Es100 | ShaderVersion::Es300), vertex_array, srgb_support, + texture_filter: Default::default(), post_process, vertex_buffer, element_array_buffer, @@ -224,6 +248,7 @@ impl Painter { gl, self.is_webgl_1, self.srgb_support, + self.texture_filter, &pixels, font_image.width, font_image.height, @@ -387,6 +412,12 @@ impl Painter { } } + // Set the filter to be used for any subsequent textures loaded via + // [`Self::set_texture`]. + pub fn set_texture_filter(&mut self, texture_filter: TextureFilter) { + self.texture_filter = texture_filter; + } + // ------------------------------------------------------------------------ #[cfg(feature = "epi")] @@ -410,6 +441,7 @@ impl Painter { gl, self.is_webgl_1, self.srgb_support, + self.texture_filter, &pixels, image.size[0], image.size[1], diff --git a/epaint/CHANGELOG.md b/epaint/CHANGELOG.md index a2088af3..90b9522c 100644 --- a/epaint/CHANGELOG.md +++ b/epaint/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to the epaint crate will be documented in this file. ## Unreleased +* Added `Shape::dashed_line_many` ([#1027](https://github.com/emilk/egui/pull/1027)). ## 0.16.0 - 2021-12-29 * Anti-alias path ends ([#893](https://github.com/emilk/egui/pull/893)). diff --git a/epaint/Cargo.toml b/epaint/Cargo.toml index 2e406ccb..debfc233 100644 --- a/epaint/Cargo.toml +++ b/epaint/Cargo.toml @@ -63,3 +63,10 @@ single_threaded = ["atomic_refcell"] # Only needed if you plan to use the same fonts from multiple threads. # It comes with a minor performance impact. multi_threaded = ["parking_lot"] + +[dev-dependencies] +criterion = { version = "0.3", default-features = false } + +[[bench]] +name = "benchmark" +harness = false diff --git a/epaint/benches/benchmark.rs b/epaint/benches/benchmark.rs new file mode 100644 index 00000000..3ae5d901 --- /dev/null +++ b/epaint/benches/benchmark.rs @@ -0,0 +1,43 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use epaint::{pos2, Color32, Shape, Stroke}; + +fn single_dashed_lines(c: &mut Criterion) { + c.bench_function("single_dashed_lines", move |b| { + b.iter(|| { + let mut v = Vec::new(); + + let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; + + for _ in 0..100 { + v.extend(Shape::dashed_line( + &line, + Stroke::new(1.5, Color32::RED), + 10.0, + 2.5, + )); + } + + black_box(v); + }); + }); +} + +fn many_dashed_lines(c: &mut Criterion) { + c.bench_function("many_dashed_lines", move |b| { + b.iter(|| { + let mut v = Vec::new(); + + let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; + + for _ in 0..100 { + Shape::dashed_line_many(&line, Stroke::new(1.5, Color32::RED), 10.0, 2.5, &mut v); + } + + black_box(v); + }); + }); +} + +criterion_group!(benches, single_dashed_lines, many_dashed_lines); +criterion_main!(benches); diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index dba2b243..fa52260e 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -75,6 +75,18 @@ impl Shape { shapes } + /// Turn a line into dashes. If you need to create many dashed lines use this instead of + /// [`Self::dashed_line`] + pub fn dashed_line_many( + points: &[Pos2], + stroke: impl Into, + dash_length: f32, + gap_length: f32, + shapes: &mut Vec, + ) { + dashes_from_line(points, stroke.into(), dash_length, gap_length, shapes); + } + /// A convex polygon with a fill and optional stroke. #[inline] pub fn convex_polygon( @@ -425,27 +437,27 @@ fn dashes_from_line( let end = window[1]; let vector = end - start; let segment_length = vector.length(); + + let mut start_point = start; while position_on_segment < segment_length { let new_point = start + vector * (position_on_segment / segment_length); if drawing_dash { // This is the end point. - if let Shape::Path(PathShape { points, .. }) = shapes.last_mut().unwrap() { - points.push(new_point); - } + shapes.push(Shape::line_segment([start_point, new_point], stroke)); position_on_segment += gap_length; } else { // Start a new dash. - shapes.push(Shape::line(vec![new_point], stroke)); + start_point = new_point; position_on_segment += dash_length; } drawing_dash = !drawing_dash; } + // If the segment ends and the dash is not finished, add the segment's end point. if drawing_dash { - if let Shape::Path(PathShape { points, .. }) = shapes.last_mut().unwrap() { - points.push(end); - } + shapes.push(Shape::line_segment([start_point, end], stroke)); } + position_on_segment -= segment_length; }); }