-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* initial work on markers * clippy fix * simplify marker * use option for color * prepare for more demo plots * more improvements for markers * some small adjustments * better highlighting * don't draw transparent lines * use transparent color instead of option * don't brighten curves when highlighting * Initial changes to lengend: * Font options * Position options * Internal cleanup * draw legend on top of curves * update changelog * fix legend checkboxes * simplify legend * remove unnecessary derives * remove config from legend entries * avoid allocations and use line_segment * compare against transparent color * create new Points primitive * fix doctest * some cleanup and fix hover * common interface for lines and points * clippy fixes * reduce visibilities * update legend * clippy fix * change instances of "curve" to "item" * change visibility * Update egui/src/widgets/plot/mod.rs Co-authored-by: Emil Ernerfeldt <[email protected]> * Update egui/src/widgets/plot/mod.rs Co-authored-by: Emil Ernerfeldt <[email protected]> * Update egui_demo_lib/src/apps/demo/plot_demo.rs Co-authored-by: Emil Ernerfeldt <[email protected]> * Update egui_demo_lib/src/apps/demo/plot_demo.rs Co-authored-by: Emil Ernerfeldt <[email protected]> * changes based on review * add legend to demo * fix test * move highlighted items to front * dynamic plot size * add legend again * remove height * clippy fix * update changelog * minor changes * Update egui/src/widgets/plot/legend.rs Co-authored-by: Emil Ernerfeldt <[email protected]> * Update egui/src/widgets/plot/legend.rs Co-authored-by: Emil Ernerfeldt <[email protected]> * Update egui/src/widgets/plot/legend.rs Co-authored-by: Emil Ernerfeldt <[email protected]> * changes based on review * add functions to mutate legend config * use horizontal_align Co-authored-by: Emil Ernerfeldt <[email protected]>
- Loading branch information
Showing
5 changed files
with
324 additions
and
110 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,81 +1,237 @@ | ||
use std::string::String; | ||
use std::{ | ||
collections::{BTreeMap, HashSet}, | ||
string::String, | ||
}; | ||
|
||
use crate::*; | ||
|
||
pub(crate) struct LegendEntry { | ||
pub text: String, | ||
pub color: Color32, | ||
pub checked: bool, | ||
pub hovered: bool, | ||
use super::items::PlotItem; | ||
|
||
/// Where to place the plot legend. | ||
#[derive(Debug, Clone, Copy, PartialEq)] | ||
pub enum Corner { | ||
LeftTop, | ||
RightTop, | ||
LeftBottom, | ||
RightBottom, | ||
} | ||
|
||
impl Corner { | ||
pub fn all() -> impl Iterator<Item = Corner> { | ||
[ | ||
Corner::LeftTop, | ||
Corner::RightTop, | ||
Corner::LeftBottom, | ||
Corner::RightBottom, | ||
] | ||
.iter() | ||
.copied() | ||
} | ||
} | ||
|
||
/// The configuration for a plot legend. | ||
#[derive(Clone, Copy, PartialEq)] | ||
pub struct Legend { | ||
pub text_style: TextStyle, | ||
pub position: Corner, | ||
} | ||
|
||
impl Default for Legend { | ||
fn default() -> Self { | ||
Self { | ||
text_style: TextStyle::Body, | ||
position: Corner::RightTop, | ||
} | ||
} | ||
} | ||
|
||
impl Legend { | ||
pub fn text_style(mut self, style: TextStyle) -> Self { | ||
self.text_style = style; | ||
self | ||
} | ||
|
||
pub fn position(mut self, corner: Corner) -> Self { | ||
self.position = corner; | ||
self | ||
} | ||
} | ||
|
||
#[derive(Clone)] | ||
struct LegendEntry { | ||
color: Color32, | ||
checked: bool, | ||
hovered: bool, | ||
} | ||
|
||
impl LegendEntry { | ||
pub fn new(text: String, color: Color32, checked: bool) -> Self { | ||
fn new(color: Color32, checked: bool) -> Self { | ||
Self { | ||
text, | ||
color, | ||
checked, | ||
hovered: false, | ||
} | ||
} | ||
} | ||
|
||
impl Widget for &mut LegendEntry { | ||
fn ui(self, ui: &mut Ui) -> Response { | ||
let LegendEntry { | ||
checked, | ||
text, | ||
fn ui(&mut self, ui: &mut Ui, text: String) -> Response { | ||
let Self { | ||
color, | ||
.. | ||
checked, | ||
hovered, | ||
} = self; | ||
let icon_width = ui.spacing().icon_width; | ||
let icon_spacing = ui.spacing().icon_spacing; | ||
let padding = vec2(2.0, 2.0); | ||
let total_extra = padding + vec2(icon_width + icon_spacing, 0.0) + padding; | ||
|
||
let text_style = TextStyle::Button; | ||
let galley = ui.fonts().layout_no_wrap(text_style, text.clone()); | ||
let galley = ui.fonts().layout_no_wrap(ui.style().body_text_style, text); | ||
|
||
let mut desired_size = total_extra + galley.size; | ||
desired_size = desired_size.at_least(ui.spacing().interact_size); | ||
desired_size.y = desired_size.y.at_least(icon_width); | ||
let icon_size = galley.size.y; | ||
let icon_spacing = icon_size / 5.0; | ||
let total_extra = vec2(icon_size + icon_spacing, 0.0); | ||
|
||
let desired_size = total_extra + galley.size; | ||
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); | ||
let rect = rect.shrink2(padding); | ||
|
||
response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, &galley.text)); | ||
|
||
let visuals = ui.style().interact(&response); | ||
let label_on_the_left = ui.layout().horizontal_align() == Align::RIGHT; | ||
|
||
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); | ||
let icon_position_x = if label_on_the_left { | ||
rect.right() - icon_size / 2.0 | ||
} else { | ||
rect.left() + icon_size / 2.0 | ||
}; | ||
let icon_position = pos2(icon_position_x, rect.center().y); | ||
let icon_rect = Rect::from_center_size(icon_position, vec2(icon_size, icon_size)); | ||
|
||
let painter = ui.painter(); | ||
|
||
painter.add(Shape::Circle { | ||
center: big_icon_rect.center(), | ||
radius: big_icon_rect.width() / 2.0 + visuals.expansion, | ||
center: icon_rect.center(), | ||
radius: icon_size * 0.5, | ||
fill: visuals.bg_fill, | ||
stroke: visuals.bg_stroke, | ||
}); | ||
|
||
if *checked { | ||
let fill = if *color == Color32::TRANSPARENT { | ||
ui.visuals().noninteractive().fg_stroke.color | ||
} else { | ||
*color | ||
}; | ||
painter.add(Shape::Circle { | ||
center: small_icon_rect.center(), | ||
radius: small_icon_rect.width() * 0.8, | ||
fill: *color, | ||
center: icon_rect.center(), | ||
radius: icon_size * 0.4, | ||
fill, | ||
stroke: Default::default(), | ||
}); | ||
} | ||
|
||
let text_position = pos2( | ||
rect.left() + padding.x + icon_width + icon_spacing, | ||
rect.center().y - 0.5 * galley.size.y, | ||
); | ||
let text_position_x = if label_on_the_left { | ||
rect.right() - icon_size - icon_spacing - galley.size.x | ||
} else { | ||
rect.left() + icon_size + icon_spacing | ||
}; | ||
|
||
let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size.y); | ||
painter.galley(text_position, galley, visuals.text_color()); | ||
|
||
self.checked ^= response.clicked_by(PointerButton::Primary); | ||
self.hovered = response.hovered(); | ||
*checked ^= response.clicked_by(PointerButton::Primary); | ||
*hovered = response.hovered(); | ||
|
||
response | ||
} | ||
} | ||
|
||
#[derive(Clone)] | ||
pub(super) struct LegendWidget { | ||
rect: Rect, | ||
entries: BTreeMap<String, LegendEntry>, | ||
config: Legend, | ||
} | ||
|
||
impl LegendWidget { | ||
/// Create a new legend from items, the names of items that are hidden and the style of the | ||
/// text. Returns `None` if the legend has no entries. | ||
pub(super) fn try_new( | ||
rect: Rect, | ||
config: Legend, | ||
items: &[Box<dyn PlotItem>], | ||
hidden_items: &HashSet<String>, | ||
) -> Option<Self> { | ||
// Collect the legend entries. If multiple items have the same name, they share a | ||
// checkbox. If their colors don't match, we pick a neutral color for the checkbox. | ||
let mut entries: BTreeMap<String, LegendEntry> = BTreeMap::new(); | ||
items | ||
.iter() | ||
.filter(|item| !item.name().is_empty()) | ||
.for_each(|item| { | ||
entries | ||
.entry(item.name().to_string()) | ||
.and_modify(|entry| { | ||
if entry.color != item.color() { | ||
// Multiple items with different colors | ||
entry.color = Color32::TRANSPARENT; | ||
} | ||
}) | ||
.or_insert_with(|| { | ||
let color = item.color(); | ||
let checked = !hidden_items.contains(item.name()); | ||
LegendEntry::new(color, checked) | ||
}); | ||
}); | ||
(!entries.is_empty()).then(|| Self { | ||
rect, | ||
entries, | ||
config, | ||
}) | ||
} | ||
|
||
// Get the names of the hidden items. | ||
pub fn get_hidden_items(&self) -> HashSet<String> { | ||
self.entries | ||
.iter() | ||
.filter(|(_, entry)| !entry.checked) | ||
.map(|(name, _)| name.clone()) | ||
.collect() | ||
} | ||
|
||
// Get the name of the hovered items. | ||
pub fn get_hovered_entry_name(&self) -> Option<String> { | ||
self.entries | ||
.iter() | ||
.find(|(_, entry)| entry.hovered) | ||
.map(|(name, _)| name.to_string()) | ||
} | ||
} | ||
|
||
impl Widget for &mut LegendWidget { | ||
fn ui(self, ui: &mut Ui) -> Response { | ||
let LegendWidget { | ||
rect, | ||
entries, | ||
config, | ||
} = self; | ||
|
||
let main_dir = match config.position { | ||
Corner::LeftTop | Corner::RightTop => Direction::TopDown, | ||
Corner::LeftBottom | Corner::RightBottom => Direction::BottomUp, | ||
}; | ||
let cross_align = match config.position { | ||
Corner::LeftTop | Corner::LeftBottom => Align::LEFT, | ||
Corner::RightTop | Corner::RightBottom => Align::RIGHT, | ||
}; | ||
let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align); | ||
let legend_pad = 2.0; | ||
let legend_rect = rect.shrink(legend_pad); | ||
let mut legend_ui = ui.child_ui(legend_rect, layout); | ||
legend_ui | ||
.scope(|ui| { | ||
ui.style_mut().body_text_style = config.text_style; | ||
entries | ||
.iter_mut() | ||
.map(|(name, entry)| entry.ui(ui, name.clone())) | ||
.reduce(|r1, r2| r1.union(r2)) | ||
.unwrap() | ||
}) | ||
.inner | ||
} | ||
} |
Oops, something went wrong.