use egui::{style::Spacing, Align, Context, Id, Pos2, Rect, Vec2}; use crate::{InPinId, OutPinId, Snarl}; use super::SnarlStyle; /// Node UI state. pub struct NodeState { /// Node size for this frame. /// It is updated to fit content. size: Vec2, header_height: f32, body_width: f32, footer_width: f32, id: Id, scale: f32, dirty: bool, } #[derive(Clone, Copy, PartialEq)] struct NodeData { unscaled_size: Vec2, unscaled_header_height: f32, unscaled_body_width: f32, unsacled_footer_width: f32, } impl NodeState { pub fn load(cx: &Context, id: Id, spacing: &Spacing, scale: f32) -> Self { match cx.data_mut(|d| d.get_temp::(id)) { Some(data) => NodeState { size: data.unscaled_size * scale, header_height: data.unscaled_header_height * scale, body_width: data.unscaled_body_width * scale, footer_width: data.unsacled_footer_width * scale, id, scale, dirty: false, }, None => Self::initial(id, spacing, scale), } } pub fn clear(self, cx: &Context) { cx.data_mut(|d| d.remove::(self.id)); } pub fn store(&self, cx: &Context) { if self.dirty { cx.data_mut(|d| { d.insert_temp( self.id, NodeData { unscaled_size: self.size / self.scale, unscaled_header_height: self.header_height / self.scale, unscaled_body_width: self.body_width / self.scale, unsacled_footer_width: self.footer_width / self.scale, }, ) }); } } /// Finds node rect at specific position (excluding node frame margin). pub fn node_rect(&self, pos: Pos2, openness: f32) -> Rect { Rect::from_min_size( pos, egui::vec2( self.size.x, f32::max(self.header_height, self.size.y * openness), ), ) } pub fn payload_offset(&self, openness: f32) -> f32 { (self.size.y) * (1.0 - openness) } pub fn align_body(&mut self, rect: Rect) -> Rect { let x_range = Align::Center.align_size_within_range(self.body_width, rect.x_range()); Rect::from_x_y_ranges(x_range, rect.y_range()) } pub fn align_footer(&mut self, rect: Rect) -> Rect { let x_range = Align::Center.align_size_within_range(self.footer_width, rect.x_range()); Rect::from_x_y_ranges(x_range, rect.y_range()) } pub fn set_size(&mut self, size: Vec2) { if self.size != size { self.size = size; self.dirty = true; } } pub fn set_header_height(&mut self, height: f32) { if self.header_height != height { self.header_height = height; self.dirty = true; } } pub fn set_body_width(&mut self, width: f32) { if self.body_width != width { self.body_width = width; self.dirty = true; } } pub fn set_footer_width(&mut self, width: f32) { if self.footer_width != width { self.footer_width = width; self.dirty = true; } } fn initial(id: Id, spacing: &Spacing, scale: f32) -> Self { NodeState { size: spacing.interact_size, header_height: spacing.interact_size.y, body_width: 0.0, footer_width: 0.0, id, dirty: true, scale, } } } #[derive(Clone)] pub enum NewWires { In(Vec), Out(Vec), } pub struct SnarlState { /// Where viewport's center in graph's space. offset: Vec2, /// Scale of the viewport. scale: f32, target_scale: f32, new_wires: Option, id: Id, /// Flag indicating that the graph state is dirty must be saved. dirty: bool, /// Flag indicating that the link menu is open. is_link_menu_open: bool, } #[derive(Clone)] struct SnarlStateData { offset: Vec2, scale: f32, target_scale: f32, new_wires: Option, is_link_menu_open: bool, } impl SnarlState { pub fn load( cx: &Context, id: Id, pivot: Pos2, viewport: Rect, snarl: &Snarl, style: &SnarlStyle, ) -> Self { let Some(mut data) = cx.data_mut(|d| d.get_temp::(id)) else { return Self::initial(id, viewport, snarl, style); }; let new_scale = cx.animate_value_with_time(id.with("zoom-scale"), data.target_scale, 0.1); let mut dirty = false; if new_scale != data.scale { let a = pivot + data.offset - viewport.center().to_vec2(); data.offset += a * new_scale / data.scale - a; data.scale = new_scale; dirty = true; } SnarlState { offset: data.offset, scale: data.scale, target_scale: data.target_scale, new_wires: data.new_wires, is_link_menu_open: data.is_link_menu_open, id, dirty, } } fn initial(id: Id, viewport: Rect, snarl: &Snarl, style: &SnarlStyle) -> Self { let mut bb = Rect::NOTHING; for (_, node) in snarl.nodes.iter() { bb.extend_with(node.pos); } if !bb.is_positive() { let scale = 1.0f32.clamp(style.min_scale, style.max_scale); return SnarlState { offset: Vec2::ZERO, scale, target_scale: scale, new_wires: None, is_link_menu_open: false, id, dirty: true, }; } bb = bb.expand(100.0); let bb_size = bb.size(); let viewport_size = viewport.size(); let scale = (viewport_size.x / bb_size.x) .min(1.0) .min(viewport_size.y / bb_size.y) .min(style.max_scale) .max(style.min_scale); let offset = bb.center().to_vec2() * scale; SnarlState { offset, scale, target_scale: scale, new_wires: None, is_link_menu_open: false, id, dirty: true, } } #[inline(always)] pub fn store(self, cx: &Context) { if self.dirty { cx.data_mut(|d| { d.insert_temp( self.id, SnarlStateData { offset: self.offset, scale: self.scale, target_scale: self.target_scale, new_wires: self.new_wires, is_link_menu_open: self.is_link_menu_open, }, ) }); } } #[inline(always)] pub fn pan(&mut self, delta: Vec2) { self.offset += delta; self.dirty = true; } #[inline(always)] pub fn scale(&self) -> f32 { self.scale } #[inline(always)] pub fn offset(&self) -> Vec2 { self.offset } #[inline(always)] pub fn set_scale(&mut self, scale: f32) { self.target_scale = scale; self.dirty = true; } #[inline(always)] pub fn screen_pos_to_graph(&self, pos: Pos2, viewport: Rect) -> Pos2 { (pos + self.offset - viewport.center().to_vec2()) / self.scale } #[inline(always)] pub fn graph_pos_to_screen(&self, pos: Pos2, viewport: Rect) -> Pos2 { pos * self.scale - self.offset + viewport.center().to_vec2() } // #[inline(always)] // pub fn graph_vec_to_screen(&self, size: Vec2) -> Vec2 { // size * self.scale // } #[inline(always)] pub fn screen_vec_to_graph(&self, size: Vec2) -> Vec2 { size / self.scale } // #[inline(always)] // pub fn graph_value_to_screen(&self, value: f32) -> f32 { // value * self.scale // } // #[inline(always)] // pub fn screen_value_to_graph(&self, value: f32) -> f32 { // value / self.scale // } pub fn start_new_wire_in(&mut self, pin: InPinId) { self.new_wires = Some(NewWires::In(vec![pin])); self.dirty = true; } pub fn start_new_wire_out(&mut self, pin: OutPinId) { self.new_wires = Some(NewWires::Out(vec![pin])); self.dirty = true; } pub fn start_new_wires_in(&mut self, pins: &[InPinId]) { self.new_wires = Some(NewWires::In(pins.to_vec())); self.dirty = true; } pub fn start_new_wires_out(&mut self, pins: &[OutPinId]) { self.new_wires = Some(NewWires::Out(pins.to_vec())); self.dirty = true; } pub fn add_new_wire_in(&mut self, pin: InPinId) { if let Some(NewWires::In(pins)) = &mut self.new_wires { if !pins.contains(&pin) { pins.push(pin); self.dirty = true; } } } pub fn add_new_wire_out(&mut self, pin: OutPinId) { if let Some(NewWires::Out(pins)) = &mut self.new_wires { if !pins.contains(&pin) { pins.push(pin); self.dirty = true; } } } pub fn remove_new_wire_in(&mut self, pin: InPinId) { if let Some(NewWires::In(pins)) = &mut self.new_wires { if let Some(idx) = pins.iter().position(|p| *p == pin) { pins.swap_remove(idx); self.dirty = true; } } } pub fn remove_new_wire_out(&mut self, pin: OutPinId) { if let Some(NewWires::Out(pins)) = &mut self.new_wires { if let Some(idx) = pins.iter().position(|p| *p == pin) { pins.swap_remove(idx); self.dirty = true; } } } pub fn has_new_wires(&self) -> bool { self.new_wires.is_some() } pub fn new_wires(&self) -> Option<&NewWires> { self.new_wires.as_ref() } pub fn take_wires(&mut self) -> Option { self.dirty |= self.new_wires.is_some(); self.new_wires.take() } pub(crate) fn revert_take_wires(&mut self, wires: NewWires) { self.new_wires = Some(wires); } pub(crate) fn open_link_menu(&mut self) { self.is_link_menu_open = true; self.dirty = true; } pub(crate) fn close_link_menu(&mut self) { self.new_wires = None; self.is_link_menu_open = false; self.dirty = true; } pub(crate) fn is_link_menu_open(&self) -> bool { self.is_link_menu_open } }