use egui::{
epaint::{CircleShape, TextShape},
Color32, FontFamily, FontId, Pos2, Shape, Stroke, Vec2,
};
use petgraph::{stable_graph::IndexType, EdgeType};
use crate::{draw::drawer::DrawContext, DisplayNode, NodeProps};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct DefaultNodeShape {
pub pos: Pos2,
pub selected: bool,
pub dragged: bool,
pub hovered: bool,
pub color: Option<Color32>,
pub label_text: String,
pub radius: f32,
}
impl<N: Clone> From<NodeProps<N>> for DefaultNodeShape {
fn from(node_props: NodeProps<N>) -> Self {
DefaultNodeShape {
pos: node_props.location(),
selected: node_props.selected,
dragged: node_props.dragged,
hovered: node_props.hovered,
label_text: node_props.label.to_string(),
color: node_props.color(),
radius: 5.0,
}
}
}
impl<N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> DisplayNode<N, E, Ty, Ix>
for DefaultNodeShape
{
fn is_inside(&self, pos: Pos2) -> bool {
is_inside_circle(self.pos, self.radius, pos)
}
fn closest_boundary_point(&self, dir: Vec2) -> Pos2 {
closest_point_on_circle(self.pos, self.radius, dir)
}
fn shapes(&mut self, ctx: &DrawContext) -> Vec<Shape> {
let mut res = Vec::with_capacity(2);
let circle_center = ctx.meta.canvas_to_screen_pos(self.pos);
let circle_radius = ctx.meta.canvas_to_screen_size(self.radius);
let color = self.effective_color(ctx);
let stroke = self.effective_stroke(ctx);
res.push(
CircleShape {
center: circle_center,
radius: circle_radius,
fill: color,
stroke,
}
.into(),
);
if !(ctx.style.labels_always || self.selected || self.dragged || self.hovered) {
return res;
}
let galley = self.label_galley(ctx, circle_radius, color);
res.push(Self::label_shape(
galley,
circle_center,
circle_radius,
color,
));
res
}
fn update(&mut self, state: &NodeProps<N>) {
self.pos = state.location();
self.selected = state.selected;
self.dragged = state.dragged;
self.hovered = state.hovered;
self.label_text = state.label.to_string();
self.color = state.color();
}
}
fn closest_point_on_circle(center: Pos2, radius: f32, dir: Vec2) -> Pos2 {
center + dir.normalized() * radius
}
fn is_inside_circle(center: Pos2, radius: f32, pos: Pos2) -> bool {
let dir = pos - center;
dir.length() <= radius
}
impl DefaultNodeShape {
fn is_interacted(&self) -> bool {
self.selected || self.dragged || self.hovered
}
fn effective_color(&self, ctx: &DrawContext) -> Color32 {
if let Some(c) = self.color {
return c;
}
let style = if self.is_interacted() {
ctx.ctx.global_style().visuals.widgets.active
} else {
ctx.ctx.global_style().visuals.widgets.inactive
};
style.fg_stroke.color
}
fn effective_stroke(&self, ctx: &DrawContext) -> Stroke {
let base = Stroke::default();
if let Some(hook) = &ctx.style.node_stroke_hook {
let style_ref: &egui::Style = &ctx.ctx.global_style();
(hook)(self.selected, self.dragged, self.color, base, style_ref)
} else {
base
}
}
fn label_galley(
&self,
ctx: &DrawContext,
radius: f32,
color: Color32,
) -> std::sync::Arc<egui::Galley> {
ctx.ctx.fonts_mut(|f| {
f.layout_no_wrap(
self.label_text.clone(),
FontId::new(radius, FontFamily::Monospace),
color,
)
})
}
fn label_shape(
galley: std::sync::Arc<egui::Galley>,
center: Pos2,
radius: f32,
color: Color32,
) -> Shape {
let label_pos = Pos2::new(center.x - galley.size().x / 2., center.y - radius * 2.);
TextShape::new(label_pos, galley, color).into()
}
}
#[cfg(test)]
mod test {
use super::*;
use egui::Pos2;
#[test]
fn test_closest_point_on_circle() {
assert_eq!(
closest_point_on_circle(Pos2::new(0.0, 0.0), 10.0, Vec2::new(5.0, 0.0)),
Pos2::new(10.0, 0.0)
);
assert_eq!(
closest_point_on_circle(Pos2::new(0.0, 0.0), 10.0, Vec2::new(15.0, 0.0)),
Pos2::new(10.0, 0.0)
);
assert_eq!(
closest_point_on_circle(Pos2::new(0.0, 0.0), 10.0, Vec2::new(0.0, 10.0)),
Pos2::new(0.0, 10.0)
);
}
#[test]
fn test_is_inside_circle() {
assert!(is_inside_circle(
Pos2::new(0.0, 0.0),
10.0,
Pos2::new(5.0, 0.0)
));
assert!(!is_inside_circle(
Pos2::new(0.0, 0.0),
10.0,
Pos2::new(15.0, 0.0)
));
assert!(is_inside_circle(
Pos2::new(0.0, 0.0),
10.0,
Pos2::new(0.0, 10.0)
));
}
}