Initial commit
This commit is contained in:
commit
6e5565d45c
16 changed files with 1548 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
|
||||||
|
Cargo.lock
|
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug executable 'virtualdevicecreator'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--bin=virtualdevicecreator",
|
||||||
|
"--package=virtualdevicecreator"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "virtualdevicecreator",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"activityBar.background": "#063147",
|
||||||
|
"titleBar.activeBackground": "#094563",
|
||||||
|
"titleBar.activeForeground": "#F6FBFE"
|
||||||
|
}
|
||||||
|
}
|
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "virtualdevicecreator"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
egui = { version = "*", features = ["persistence"] }
|
||||||
|
eframe = "*"
|
||||||
|
epi = "*"
|
||||||
|
slotmap = { version = "*", features = ["serde"] }
|
||||||
|
smallvec = { version = "*", features = ["serde"] }
|
||||||
|
strum = "*"
|
||||||
|
strum_macros = "*"
|
||||||
|
serde = { version = "*", features = ["derive"] }
|
||||||
|
anyhow = "*"
|
||||||
|
nalgebra = { version = "*", features = ["serde-serialize"] }
|
||||||
|
rfd = "*"
|
93
src/color_hex_utils.rs
Normal file
93
src/color_hex_utils.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
use egui::Color32;
|
||||||
|
|
||||||
|
/// Converts a hex string with a leading '#' into a egui::Color32.
|
||||||
|
/// - The first three channels are interpreted as R, G, B.
|
||||||
|
/// - The fourth channel, if present, is used as the alpha value.
|
||||||
|
/// - Both upper and lowercase characters can be used for the hex values.
|
||||||
|
///
|
||||||
|
/// *Adapted from: https://docs.rs/raster/0.1.0/src/raster/lib.rs.html#425-725.
|
||||||
|
/// Credit goes to original authors.*
|
||||||
|
pub fn color_from_hex(hex: &str) -> Result<Color32, String> {
|
||||||
|
// Convert a hex string to decimal. Eg. "00" -> 0. "FF" -> 255.
|
||||||
|
fn _hex_dec(hex_string: &str) -> Result<u8, String> {
|
||||||
|
match u8::from_str_radix(hex_string, 16) {
|
||||||
|
Ok(o) => Ok(o),
|
||||||
|
Err(e) => Err(format!("Error parsing hex: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hex.len() == 9 && hex.starts_with('#') {
|
||||||
|
// #FFFFFFFF (Red Green Blue Alpha)
|
||||||
|
return Ok(Color32::from_rgba_premultiplied(
|
||||||
|
_hex_dec(&hex[1..3])?,
|
||||||
|
_hex_dec(&hex[3..5])?,
|
||||||
|
_hex_dec(&hex[5..7])?,
|
||||||
|
_hex_dec(&hex[7..9])?,
|
||||||
|
));
|
||||||
|
} else if hex.len() == 7 && hex.starts_with('#') {
|
||||||
|
// #FFFFFF (Red Green Blue)
|
||||||
|
return Ok(Color32::from_rgb(
|
||||||
|
_hex_dec(&hex[1..3])?,
|
||||||
|
_hex_dec(&hex[3..5])?,
|
||||||
|
_hex_dec(&hex[5..7])?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!(
|
||||||
|
"Error parsing hex: {}. Example of valid formats: #FFFFFF or #ffffffff",
|
||||||
|
hex
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a Color32 into its canonical hexadecimal representation.
|
||||||
|
/// - The color string will be preceded by '#'.
|
||||||
|
/// - If the alpha channel is completely opaque, it will be ommitted.
|
||||||
|
/// - Characters from 'a' to 'f' will be written in lowercase.
|
||||||
|
pub fn color_to_hex(color: Color32) -> String {
|
||||||
|
if color.a() < 255 {
|
||||||
|
format!(
|
||||||
|
"#{:02x?}{:02x?}{:02x?}{:02x?}",
|
||||||
|
color.r(),
|
||||||
|
color.g(),
|
||||||
|
color.b(),
|
||||||
|
color.a()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("#{:02x?}{:02x?}{:02x?}", color.r(), color.g(), color.b())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_color_from_and_to_hex() {
|
||||||
|
assert_eq!(
|
||||||
|
color_from_hex("#00ff00").unwrap(),
|
||||||
|
Color32::from_rgb(0, 255, 0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
color_from_hex("#5577AA").unwrap(),
|
||||||
|
Color32::from_rgb(85, 119, 170)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
color_from_hex("#E2e2e277").unwrap(),
|
||||||
|
Color32::from_rgba_premultiplied(226, 226, 226, 119)
|
||||||
|
);
|
||||||
|
assert!(color_from_hex("abcdefgh").is_err());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
color_to_hex(Color32::from_rgb(0, 255, 0)),
|
||||||
|
"#00ff00".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
color_to_hex(Color32::from_rgb(85, 119, 170)),
|
||||||
|
"#5577aa".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
color_to_hex(Color32::from_rgba_premultiplied(226, 226, 226, 119)),
|
||||||
|
"e2e2e277".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
69
src/editor_state.rs
Normal file
69
src/editor_state.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::graph_types::*;
|
||||||
|
use crate::id_types::*;
|
||||||
|
use crate::node_finder::NodeFinder;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct PanZoom {
|
||||||
|
pub pan: egui::Vec2,
|
||||||
|
pub zoom: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GraphEditorState {
|
||||||
|
pub graph: Graph,
|
||||||
|
/// An ongoing connection interaction: The mouse has dragged away from a
|
||||||
|
/// port and the user is holding the click
|
||||||
|
pub connection_in_progress: Option<(NodeId, AnyParameterId)>,
|
||||||
|
/// The currently active node. A program will be compiled to compute the
|
||||||
|
/// result of this node and constantly updated in real-time.
|
||||||
|
pub active_node: Option<NodeId>,
|
||||||
|
/// The position of each node.
|
||||||
|
pub node_positions: HashMap<NodeId, egui::Pos2>,
|
||||||
|
/// The node finder is used to create new nodes.
|
||||||
|
pub node_finder: Option<NodeFinder>,
|
||||||
|
/// When this option is set by the UI, the side effect encoded by the node
|
||||||
|
/// will be executed at the start of the next frame.
|
||||||
|
pub run_side_effect: Option<NodeId>,
|
||||||
|
/// The panning of the graph viewport.
|
||||||
|
pub pan_zoom: PanZoom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphEditorState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
graph: Graph::new(),
|
||||||
|
connection_in_progress: None,
|
||||||
|
active_node: None,
|
||||||
|
run_side_effect: None,
|
||||||
|
node_positions: HashMap::new(),
|
||||||
|
node_finder: None,
|
||||||
|
pan_zoom: PanZoom {
|
||||||
|
pan: egui::Vec2::ZERO,
|
||||||
|
zoom: 1.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GraphEditorState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PanZoom {
|
||||||
|
pub fn adjust_zoom(
|
||||||
|
&mut self,
|
||||||
|
zoom_delta: f32,
|
||||||
|
point: egui::Vec2,
|
||||||
|
zoom_min: f32,
|
||||||
|
zoom_max: f32,
|
||||||
|
) {
|
||||||
|
let zoom_clamped = (self.zoom + zoom_delta).clamp(zoom_min, zoom_max);
|
||||||
|
let zoom_delta = zoom_clamped - self.zoom;
|
||||||
|
|
||||||
|
self.zoom += zoom_delta;
|
||||||
|
self.pan += point * zoom_delta;
|
||||||
|
}
|
||||||
|
}
|
160
src/graph_editor_egui.rs
Normal file
160
src/graph_editor_egui.rs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
use egui::*;
|
||||||
|
|
||||||
|
use crate::color_hex_utils::*;
|
||||||
|
use crate::editor_state::GraphEditorState;
|
||||||
|
use crate::graph_node_ui::*;
|
||||||
|
use crate::id_types::*;
|
||||||
|
use crate::node_finder::NodeFinder;
|
||||||
|
|
||||||
|
pub fn draw_graph_editor(ctx: &CtxRef, state: &mut GraphEditorState) {
|
||||||
|
let mouse = &ctx.input().pointer;
|
||||||
|
let cursor_pos = mouse.hover_pos().unwrap_or(Pos2::ZERO);
|
||||||
|
|
||||||
|
// Gets filled with the port locations as nodes are drawn
|
||||||
|
let mut port_locations = PortLocations::new();
|
||||||
|
|
||||||
|
// The responses returned from node drawing have side effects that are best
|
||||||
|
// executed at the end of this function.
|
||||||
|
let mut delayed_responses: Vec<DrawGraphNodeResponse> = vec![];
|
||||||
|
|
||||||
|
CentralPanel::default().show(ctx, |ui| {
|
||||||
|
/* Draw nodes */
|
||||||
|
let nodes = state.graph.iter_nodes().collect::<Vec<_>>(); // avoid borrow checker
|
||||||
|
for node_id in nodes {
|
||||||
|
let response = GraphNodeWidget {
|
||||||
|
position: state.node_positions.get_mut(&node_id).unwrap(),
|
||||||
|
graph: &mut state.graph,
|
||||||
|
port_locations: &mut port_locations,
|
||||||
|
node_id,
|
||||||
|
ongoing_drag: state.connection_in_progress,
|
||||||
|
active: state
|
||||||
|
.active_node
|
||||||
|
.map(|active| active == node_id)
|
||||||
|
.unwrap_or(false),
|
||||||
|
pan: state.pan_zoom.pan,
|
||||||
|
}
|
||||||
|
.show(ui);
|
||||||
|
|
||||||
|
if let Some(response) = response {
|
||||||
|
delayed_responses.push(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Draw the node finder, if open */
|
||||||
|
let mut should_close_node_finder = false;
|
||||||
|
if let Some(ref mut node_finder) = state.node_finder {
|
||||||
|
let mut node_finder_area = Area::new("node_finder");
|
||||||
|
if let Some(pos) = node_finder.position {
|
||||||
|
node_finder_area = node_finder_area.current_pos(pos);
|
||||||
|
}
|
||||||
|
node_finder_area.show(ctx, |ui| {
|
||||||
|
if let Some(node_archetype) = node_finder.show(ui) {
|
||||||
|
let new_node = state.graph.add_node(node_archetype.to_descriptor());
|
||||||
|
state
|
||||||
|
.node_positions
|
||||||
|
.insert(new_node, cursor_pos - state.pan_zoom.pan);
|
||||||
|
should_close_node_finder = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if should_close_node_finder {
|
||||||
|
state.node_finder = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Draw connections */
|
||||||
|
let connection_stroke = egui::Stroke {
|
||||||
|
width: 5.0,
|
||||||
|
color: color_from_hex("#efefef").unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((_, ref locator)) = state.connection_in_progress {
|
||||||
|
let painter = ctx.layer_painter(LayerId::background());
|
||||||
|
let start_pos = port_locations[locator];
|
||||||
|
painter.line_segment([start_pos, cursor_pos], connection_stroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (input, output) in state.graph.iter_connections() {
|
||||||
|
let painter = ctx.layer_painter(LayerId::background());
|
||||||
|
let src_pos = port_locations[&AnyParameterId::Output(output)];
|
||||||
|
let dst_pos = port_locations[&AnyParameterId::Input(input)];
|
||||||
|
painter.line_segment([src_pos, dst_pos], connection_stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle responses from drawing nodes */
|
||||||
|
|
||||||
|
for response in delayed_responses {
|
||||||
|
match response {
|
||||||
|
DrawGraphNodeResponse::ConnectEventStarted(node_id, port) => {
|
||||||
|
state.connection_in_progress = Some((node_id, port));
|
||||||
|
}
|
||||||
|
DrawGraphNodeResponse::ConnectEventEnded(locator) => {
|
||||||
|
let in_out = match (
|
||||||
|
state
|
||||||
|
.connection_in_progress
|
||||||
|
.map(|(_node, param)| param)
|
||||||
|
.take()
|
||||||
|
.expect("Cannot end drag without in-progress connection."),
|
||||||
|
locator,
|
||||||
|
) {
|
||||||
|
(AnyParameterId::Input(input), AnyParameterId::Output(output))
|
||||||
|
| (AnyParameterId::Output(output), AnyParameterId::Input(input)) => {
|
||||||
|
Some((input, output))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((input, output)) = in_out {
|
||||||
|
state.graph.add_connection(output, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DrawGraphNodeResponse::SetActiveNode(node_id) => {
|
||||||
|
state.active_node = Some(node_id);
|
||||||
|
}
|
||||||
|
DrawGraphNodeResponse::ClearActiveNode => {
|
||||||
|
state.active_node = None;
|
||||||
|
}
|
||||||
|
DrawGraphNodeResponse::RunNodeSideEffect(node_id) => {
|
||||||
|
state.run_side_effect = Some(node_id);
|
||||||
|
}
|
||||||
|
DrawGraphNodeResponse::DeleteNode(node_id) => {
|
||||||
|
state.graph.remove_node(node_id);
|
||||||
|
state.node_positions.remove(&node_id);
|
||||||
|
// Make sure to not leave references to old nodes hanging
|
||||||
|
if state.active_node.map(|x| x == node_id).unwrap_or(false) {
|
||||||
|
state.active_node = None;
|
||||||
|
}
|
||||||
|
if state.run_side_effect.map(|x| x == node_id).unwrap_or(false) {
|
||||||
|
state.run_side_effect = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DrawGraphNodeResponse::DisconnectEvent(input_id) => {
|
||||||
|
let corresp_output = state
|
||||||
|
.graph
|
||||||
|
.connection(input_id)
|
||||||
|
.expect("Connection data should be valid");
|
||||||
|
let other_node = state.graph.get_input(input_id).node();
|
||||||
|
state.graph.remove_connection(input_id);
|
||||||
|
state.connection_in_progress =
|
||||||
|
Some((other_node, AnyParameterId::Output(corresp_output)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mouse input handling */
|
||||||
|
|
||||||
|
if mouse.any_released() && state.connection_in_progress.is_some() {
|
||||||
|
state.connection_in_progress = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if mouse.button_down(PointerButton::Secondary) {
|
||||||
|
state.node_finder = Some(NodeFinder::new_at(cursor_pos));
|
||||||
|
}
|
||||||
|
if ctx.input().key_pressed(Key::Escape) {
|
||||||
|
state.node_finder = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.input().pointer.middle_down() {
|
||||||
|
state.pan_zoom.pan += ctx.input().pointer.delta();
|
||||||
|
}
|
||||||
|
}
|
203
src/graph_impls.rs
Normal file
203
src/graph_impls.rs
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use smallvec::smallvec;
|
||||||
|
|
||||||
|
use crate::graph_types::*;
|
||||||
|
use crate::id_types::*;
|
||||||
|
use crate::SVec;
|
||||||
|
|
||||||
|
impl Graph {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_node(&mut self, d: NodeDescriptor) -> NodeId {
|
||||||
|
let node_id = self.nodes.insert_with_key(|node_id| {
|
||||||
|
Node {
|
||||||
|
id: node_id,
|
||||||
|
label: d.label,
|
||||||
|
op_name: d.op_name,
|
||||||
|
// These will get filled in later
|
||||||
|
inputs: Vec::default(),
|
||||||
|
outputs: Vec::default(),
|
||||||
|
is_executable: d.is_executable,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
use InputParamKind::*;
|
||||||
|
let inputs: Vec<(String, InputId)> = d
|
||||||
|
.inputs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(input_name, input)| {
|
||||||
|
let input_id = self.inputs.insert_with_key(|id| match input {
|
||||||
|
InputDescriptor::Vector { default } => InputParam {
|
||||||
|
id,
|
||||||
|
typ: DataType::Vector,
|
||||||
|
value: InputParamValue::Vector(default),
|
||||||
|
metadata: smallvec![],
|
||||||
|
kind: ConnectionOrConstant,
|
||||||
|
node: node_id,
|
||||||
|
},
|
||||||
|
InputDescriptor::Mesh => InputParam {
|
||||||
|
id,
|
||||||
|
typ: DataType::Mesh,
|
||||||
|
value: InputParamValue::None,
|
||||||
|
metadata: smallvec![],
|
||||||
|
kind: ConnectionOnly,
|
||||||
|
node: node_id,
|
||||||
|
},
|
||||||
|
InputDescriptor::Selection => InputParam {
|
||||||
|
id,
|
||||||
|
typ: DataType::Selection,
|
||||||
|
value: InputParamValue::Selection {
|
||||||
|
text: "".into(),
|
||||||
|
selection: Some(vec![]),
|
||||||
|
},
|
||||||
|
metadata: smallvec![],
|
||||||
|
kind: ConnectionOrConstant,
|
||||||
|
node: node_id,
|
||||||
|
},
|
||||||
|
InputDescriptor::Scalar { default, min, max } => InputParam {
|
||||||
|
id,
|
||||||
|
typ: DataType::Scalar,
|
||||||
|
value: InputParamValue::Scalar(default),
|
||||||
|
metadata: smallvec![InputParamMetadata::MinMaxScalar { min, max }],
|
||||||
|
kind: ConnectionOrConstant,
|
||||||
|
node: node_id,
|
||||||
|
},
|
||||||
|
InputDescriptor::Enum { values } => InputParam {
|
||||||
|
id,
|
||||||
|
typ: DataType::Enum,
|
||||||
|
value: InputParamValue::Enum {
|
||||||
|
values,
|
||||||
|
selection: None,
|
||||||
|
},
|
||||||
|
metadata: smallvec![],
|
||||||
|
kind: ConstantOnly,
|
||||||
|
node: node_id,
|
||||||
|
},
|
||||||
|
InputDescriptor::NewFile => InputParam {
|
||||||
|
id,
|
||||||
|
typ: DataType::NewFile,
|
||||||
|
value: InputParamValue::NewFile { path: None },
|
||||||
|
metadata: smallvec![],
|
||||||
|
kind: ConstantOnly,
|
||||||
|
node: node_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(input_name, input_id)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let outputs: Vec<(String, OutputId)> = d
|
||||||
|
.outputs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(output_name, output)| {
|
||||||
|
let output_id = self.outputs.insert_with_key(|id| OutputParam {
|
||||||
|
node: node_id,
|
||||||
|
id,
|
||||||
|
typ: output.0,
|
||||||
|
});
|
||||||
|
(output_name, output_id)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self[node_id].inputs = inputs;
|
||||||
|
self[node_id].outputs = outputs;
|
||||||
|
node_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_node(&mut self, node_id: NodeId) {
|
||||||
|
self.connections
|
||||||
|
.retain(|i, o| !(self.outputs[*o].node == node_id || self.inputs[*i].node == node_id));
|
||||||
|
let inputs: SVec<_> = self[node_id].input_ids().collect();
|
||||||
|
for input in inputs {
|
||||||
|
self.inputs.remove(input);
|
||||||
|
}
|
||||||
|
let outputs: SVec<_> = self[node_id].output_ids().collect();
|
||||||
|
for output in outputs {
|
||||||
|
self.outputs.remove(output);
|
||||||
|
}
|
||||||
|
self.nodes.remove(node_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_connection(&mut self, input_id: InputId) -> Option<OutputId> {
|
||||||
|
self.connections.remove(&input_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
|
||||||
|
self.nodes.iter().map(|(id, _)| id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_connection(&mut self, output: OutputId, input: InputId) {
|
||||||
|
self.connections.insert(input, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter_connections(&self) -> impl Iterator<Item = (InputId, OutputId)> + '_ {
|
||||||
|
self.connections.iter().map(|(o, i)| (*o, *i))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connection(&self, input: InputId) -> Option<OutputId> {
|
||||||
|
self.connections.get(&input).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn any_param_type(&self, param: AnyParameterId) -> Result<DataType> {
|
||||||
|
match param {
|
||||||
|
AnyParameterId::Input(input) => self.inputs.get(input).map(|x| x.typ),
|
||||||
|
AnyParameterId::Output(output) => self.outputs.get(output).map(|x| x.typ),
|
||||||
|
}
|
||||||
|
.ok_or_else(|| anyhow!("Invalid parameter id: {:?}", param))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_input(&self, input: InputId) -> &InputParam {
|
||||||
|
&self.inputs[input]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_output(&self, output: OutputId) -> &OutputParam {
|
||||||
|
&self.outputs[output]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
pub fn inputs<'a>(&'a self, graph: &'a Graph) -> impl Iterator<Item = &InputParam> + 'a {
|
||||||
|
self.input_ids().map(|id| graph.get_input(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn outputs<'a>(&'a self, graph: &'a Graph) -> impl Iterator<Item = &OutputParam> + 'a {
|
||||||
|
self.output_ids().map(|id| graph.get_output(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn input_ids(&self) -> impl Iterator<Item = InputId> + '_ {
|
||||||
|
self.inputs.iter().map(|(_name, id)| *id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_ids(&self) -> impl Iterator<Item = OutputId> + '_ {
|
||||||
|
self.outputs.iter().map(|(_name, id)| *id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_input(&self, name: &str) -> Result<InputId> {
|
||||||
|
self.inputs
|
||||||
|
.iter()
|
||||||
|
.find(|(param_name, _id)| param_name == name)
|
||||||
|
.map(|x| x.1)
|
||||||
|
.ok_or_else(|| anyhow!("Node {:?} has no parameter named {}", self.id, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_output(&self, name: &str) -> Result<OutputId> {
|
||||||
|
self.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|(param_name, _id)| param_name == name)
|
||||||
|
.map(|x| x.1)
|
||||||
|
.ok_or_else(|| anyhow!("Node {:?} has no parameter named {}", self.id, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Can this node be enabled on the UI? I.e. does it output a mesh?
|
||||||
|
pub fn can_be_enabled(&self, graph: &Graph) -> bool {
|
||||||
|
self.outputs(graph)
|
||||||
|
.any(|output| output.typ == DataType::Mesh)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executable nodes are used to produce side effects, like exporting files.
|
||||||
|
pub fn is_executable(&self) -> bool {
|
||||||
|
self.is_executable
|
||||||
|
}
|
||||||
|
}
|
359
src/graph_node_ui.rs
Normal file
359
src/graph_node_ui.rs
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
use egui::*;
|
||||||
|
use epaint::*;
|
||||||
|
|
||||||
|
use crate::color_hex_utils::*;
|
||||||
|
use crate::graph_types::*;
|
||||||
|
use crate::id_types::*;
|
||||||
|
|
||||||
|
pub type PortLocations = std::collections::HashMap<AnyParameterId, Pos2>;
|
||||||
|
|
||||||
|
pub enum DrawGraphNodeResponse {
|
||||||
|
ConnectEventStarted(NodeId, AnyParameterId),
|
||||||
|
ConnectEventEnded(AnyParameterId),
|
||||||
|
SetActiveNode(NodeId),
|
||||||
|
RunNodeSideEffect(NodeId),
|
||||||
|
ClearActiveNode,
|
||||||
|
DeleteNode(NodeId),
|
||||||
|
DisconnectEvent(InputId),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GraphNodeWidget<'a> {
|
||||||
|
pub position: &'a mut Pos2,
|
||||||
|
pub graph: &'a mut Graph,
|
||||||
|
pub port_locations: &'a mut PortLocations,
|
||||||
|
pub node_id: NodeId,
|
||||||
|
pub ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
||||||
|
pub active: bool,
|
||||||
|
pub pan: egui::Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GraphNodeWidget<'a> {
|
||||||
|
pub const MAX_NODE_SIZE: [f32; 2] = [200.0, 200.0];
|
||||||
|
|
||||||
|
pub fn show(self, ui: &mut Ui) -> Option<DrawGraphNodeResponse> {
|
||||||
|
let mut child_ui = ui.child_ui(
|
||||||
|
Rect::from_min_size(*self.position + self.pan, Self::MAX_NODE_SIZE.into()),
|
||||||
|
Layout::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let node_resp = Self::show_graph_node(
|
||||||
|
self.graph,
|
||||||
|
self.node_id,
|
||||||
|
&mut child_ui,
|
||||||
|
self.port_locations,
|
||||||
|
self.ongoing_drag,
|
||||||
|
self.active,
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = ui.allocate_rect(child_ui.min_rect(), Sense::drag());
|
||||||
|
*self.position += resp.drag_delta();
|
||||||
|
|
||||||
|
node_resp
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws this node. Also fills in the list of port locations with all of its ports.
|
||||||
|
/// Returns a response showing whether a drag event was started.
|
||||||
|
/// Parameters:
|
||||||
|
/// - **ongoing_drag**: Is there a port drag event currently going on?
|
||||||
|
fn show_graph_node(
|
||||||
|
graph: &mut Graph,
|
||||||
|
node_id: NodeId,
|
||||||
|
ui: &mut Ui,
|
||||||
|
port_locations: &mut PortLocations,
|
||||||
|
ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
||||||
|
active: bool,
|
||||||
|
) -> Option<DrawGraphNodeResponse> {
|
||||||
|
let margin = egui::vec2(15.0, 5.0);
|
||||||
|
let _field_separation = 5.0;
|
||||||
|
let mut response: Option<DrawGraphNodeResponse> = None;
|
||||||
|
|
||||||
|
let background_color = color_from_hex("#3f3f3f").unwrap();
|
||||||
|
let titlebar_color = background_color.linear_multiply(0.8);
|
||||||
|
let text_color = color_from_hex("#fefefe").unwrap();
|
||||||
|
|
||||||
|
ui.visuals_mut().widgets.noninteractive.fg_stroke = Stroke::new(2.0, text_color);
|
||||||
|
|
||||||
|
// Preallocate shapes to paint below contents
|
||||||
|
let background_shape = ui.painter().add(Shape::Noop);
|
||||||
|
|
||||||
|
let outer_rect_bounds = ui.available_rect_before_wrap();
|
||||||
|
let mut inner_rect = outer_rect_bounds.shrink2(margin);
|
||||||
|
|
||||||
|
// Make sure we don't shrink to the negative:
|
||||||
|
inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x);
|
||||||
|
inner_rect.max.y = inner_rect.max.y.max(inner_rect.min.y);
|
||||||
|
|
||||||
|
let mut child_ui = ui.child_ui(inner_rect, *ui.layout());
|
||||||
|
let mut title_height = 0.0;
|
||||||
|
|
||||||
|
let mut input_port_heights = vec![];
|
||||||
|
let mut output_port_heights = vec![];
|
||||||
|
|
||||||
|
child_ui.vertical(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(Label::new(
|
||||||
|
RichText::new(&graph[node_id].label)
|
||||||
|
.text_style(TextStyle::Button)
|
||||||
|
.color(color_from_hex("#fefefe").unwrap()),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
ui.add_space(margin.y);
|
||||||
|
title_height = ui.min_size().y;
|
||||||
|
|
||||||
|
// First pass: Draw the inner fields. Compute port heights
|
||||||
|
let inputs = graph[node_id].inputs.clone();
|
||||||
|
for (param_name, param) in inputs {
|
||||||
|
let height_before = ui.min_rect().bottom();
|
||||||
|
if graph.connection(param).is_some() {
|
||||||
|
ui.label(param_name);
|
||||||
|
} else {
|
||||||
|
graph[param].value_widget(¶m_name, ui);
|
||||||
|
}
|
||||||
|
let height_after = ui.min_rect().bottom();
|
||||||
|
input_port_heights.push((height_before + height_after) / 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputs = graph[node_id].outputs.clone();
|
||||||
|
for (param_name, _param) in outputs {
|
||||||
|
let height_before = ui.min_rect().bottom();
|
||||||
|
ui.label(¶m_name);
|
||||||
|
let height_after = ui.min_rect().bottom();
|
||||||
|
output_port_heights.push((height_before + height_after) / 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button row
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// Show 'Enable' button for nodes that output a mesh
|
||||||
|
if graph[node_id].can_be_enabled(graph) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if !active {
|
||||||
|
if ui.button("👁 Set active").clicked() {
|
||||||
|
response = Some(DrawGraphNodeResponse::SetActiveNode(node_id));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let button = egui::Button::new(
|
||||||
|
RichText::new("👁 Active").color(egui::Color32::BLACK),
|
||||||
|
)
|
||||||
|
.fill(egui::Color32::GOLD);
|
||||||
|
if ui.add(button).clicked() {
|
||||||
|
response = Some(DrawGraphNodeResponse::ClearActiveNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Show 'Run' button for executable nodes
|
||||||
|
if graph[node_id].is_executable() && ui.button("⛭ Run").clicked() {
|
||||||
|
response = Some(DrawGraphNodeResponse::RunNodeSideEffect(node_id));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Second pass, iterate again to draw the ports. This happens outside
|
||||||
|
// the child_ui because we want ports to overflow the node background.
|
||||||
|
|
||||||
|
let outer_rect = child_ui.min_rect().expand2(margin);
|
||||||
|
let port_left = outer_rect.left();
|
||||||
|
let port_right = outer_rect.right();
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn draw_port(
|
||||||
|
ui: &mut Ui,
|
||||||
|
graph: &Graph,
|
||||||
|
node_id: NodeId,
|
||||||
|
port_pos: Pos2,
|
||||||
|
response: &mut Option<DrawGraphNodeResponse>,
|
||||||
|
param_id: AnyParameterId,
|
||||||
|
port_locations: &mut PortLocations,
|
||||||
|
ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
||||||
|
is_connected_input: bool,
|
||||||
|
) {
|
||||||
|
let port_type = graph.any_param_type(param_id).unwrap();
|
||||||
|
|
||||||
|
let port_rect = Rect::from_center_size(port_pos, egui::vec2(10.0, 10.0));
|
||||||
|
|
||||||
|
let sense = if ongoing_drag.is_some() {
|
||||||
|
Sense::hover()
|
||||||
|
} else {
|
||||||
|
Sense::click_and_drag()
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = ui.allocate_rect(port_rect, sense);
|
||||||
|
let port_color = if resp.hovered() {
|
||||||
|
Color32::WHITE
|
||||||
|
} else {
|
||||||
|
GraphNodeWidget::data_type_color(port_type)
|
||||||
|
};
|
||||||
|
ui.painter()
|
||||||
|
.circle(port_rect.center(), 5.0, port_color, Stroke::none());
|
||||||
|
|
||||||
|
if resp.drag_started() {
|
||||||
|
if is_connected_input {
|
||||||
|
*response = Some(DrawGraphNodeResponse::DisconnectEvent(
|
||||||
|
param_id.assume_input(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
*response = Some(DrawGraphNodeResponse::ConnectEventStarted(
|
||||||
|
node_id, param_id,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((origin_node, origin_param)) = ongoing_drag {
|
||||||
|
if origin_node != node_id {
|
||||||
|
// Don't allow self-loops
|
||||||
|
if graph.any_param_type(origin_param).unwrap() == port_type
|
||||||
|
&& resp.hovered()
|
||||||
|
&& ui.input().pointer.any_released()
|
||||||
|
{
|
||||||
|
*response = Some(DrawGraphNodeResponse::ConnectEventEnded(param_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
port_locations.insert(param_id, port_rect.center());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input ports
|
||||||
|
for ((_, param), port_height) in graph[node_id]
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.zip(input_port_heights.into_iter())
|
||||||
|
{
|
||||||
|
let should_draw = match graph[*param].kind() {
|
||||||
|
InputParamKind::ConnectionOnly => true,
|
||||||
|
InputParamKind::ConstantOnly => false,
|
||||||
|
InputParamKind::ConnectionOrConstant => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_draw {
|
||||||
|
let pos_left = pos2(port_left, port_height);
|
||||||
|
draw_port(
|
||||||
|
ui,
|
||||||
|
graph,
|
||||||
|
node_id,
|
||||||
|
pos_left,
|
||||||
|
&mut response,
|
||||||
|
AnyParameterId::Input(*param),
|
||||||
|
port_locations,
|
||||||
|
ongoing_drag,
|
||||||
|
graph.connection(*param).is_some(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output ports
|
||||||
|
for ((_, param), port_height) in graph[node_id]
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.zip(output_port_heights.into_iter())
|
||||||
|
{
|
||||||
|
let pos_right = pos2(port_right, port_height);
|
||||||
|
draw_port(
|
||||||
|
ui,
|
||||||
|
graph,
|
||||||
|
node_id,
|
||||||
|
pos_right,
|
||||||
|
&mut response,
|
||||||
|
AnyParameterId::Output(*param),
|
||||||
|
port_locations,
|
||||||
|
ongoing_drag,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the background shape.
|
||||||
|
// NOTE: This code is a bit more involve than it needs to be because egui
|
||||||
|
// does not support drawing rectangles with asymmetrical round corners.
|
||||||
|
|
||||||
|
let shape = {
|
||||||
|
let corner_radius = 4.0;
|
||||||
|
|
||||||
|
let titlebar_height = title_height + margin.y;
|
||||||
|
let titlebar_rect =
|
||||||
|
Rect::from_min_size(outer_rect.min, vec2(outer_rect.width(), titlebar_height));
|
||||||
|
let titlebar = Shape::Rect(RectShape {
|
||||||
|
rect: titlebar_rect,
|
||||||
|
corner_radius,
|
||||||
|
fill: titlebar_color,
|
||||||
|
stroke: Stroke::none(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let body_rect = Rect::from_min_size(
|
||||||
|
outer_rect.min + vec2(0.0, titlebar_height - corner_radius),
|
||||||
|
vec2(outer_rect.width(), outer_rect.height() - titlebar_height),
|
||||||
|
);
|
||||||
|
let body = Shape::Rect(RectShape {
|
||||||
|
rect: body_rect,
|
||||||
|
corner_radius: 0.0,
|
||||||
|
fill: background_color,
|
||||||
|
stroke: Stroke::none(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let bottom_body_rect = Rect::from_min_size(
|
||||||
|
body_rect.min + vec2(0.0, body_rect.height() - titlebar_height * 0.5),
|
||||||
|
vec2(outer_rect.width(), titlebar_height),
|
||||||
|
);
|
||||||
|
let bottom_body = Shape::Rect(RectShape {
|
||||||
|
rect: bottom_body_rect,
|
||||||
|
corner_radius,
|
||||||
|
fill: background_color,
|
||||||
|
stroke: Stroke::none(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Shape::Vec(vec![titlebar, body, bottom_body])
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.painter().set(background_shape, shape);
|
||||||
|
ui.allocate_rect(outer_rect, Sense::hover());
|
||||||
|
|
||||||
|
// Titlebar buttons
|
||||||
|
if Self::close_button(ui, outer_rect).clicked() {
|
||||||
|
response = Some(DrawGraphNodeResponse::DeleteNode(node_id));
|
||||||
|
};
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_button(ui: &mut Ui, node_rect: Rect) -> Response {
|
||||||
|
// Measurements
|
||||||
|
let margin = 8.0;
|
||||||
|
let size = 10.0;
|
||||||
|
let stroke_width = 2.0;
|
||||||
|
let offs = margin + size / 2.0;
|
||||||
|
|
||||||
|
let position = pos2(node_rect.right() - offs, node_rect.top() + offs);
|
||||||
|
let rect = Rect::from_center_size(position, vec2(size, size));
|
||||||
|
let resp = ui.allocate_rect(rect, Sense::click());
|
||||||
|
|
||||||
|
let color = if resp.clicked() {
|
||||||
|
color_from_hex("#ffffff").unwrap()
|
||||||
|
} else if resp.hovered() {
|
||||||
|
color_from_hex("#dddddd").unwrap()
|
||||||
|
} else {
|
||||||
|
color_from_hex("#aaaaaa").unwrap()
|
||||||
|
};
|
||||||
|
let stroke = Stroke {
|
||||||
|
width: stroke_width,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.painter()
|
||||||
|
.line_segment([rect.left_top(), rect.right_bottom()], stroke);
|
||||||
|
ui.painter()
|
||||||
|
.line_segment([rect.right_top(), rect.left_bottom()], stroke);
|
||||||
|
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The port colors for all the data types
|
||||||
|
fn data_type_color(data_type: DataType) -> egui::Color32 {
|
||||||
|
match data_type {
|
||||||
|
DataType::Mesh => color_from_hex("#266dd3").unwrap(),
|
||||||
|
DataType::Vector => color_from_hex("#eecf6d").unwrap(),
|
||||||
|
DataType::Scalar => color_from_hex("#eb9fef").unwrap(),
|
||||||
|
DataType::Selection => color_from_hex("#4b7f52").unwrap(),
|
||||||
|
DataType::Enum => color_from_hex("#ff0000").unwrap(), // Should never be in a port, so highlight in red
|
||||||
|
DataType::NewFile => color_from_hex("#ff0000").unwrap(), // Should never be in a port, so highlight in red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
src/graph_types.rs
Normal file
141
src/graph_types.rs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
use nalgebra::Vector3 as Vec3;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use slotmap::SlotMap;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::id_types::*;
|
||||||
|
use crate::SVec;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum DataType {
|
||||||
|
Vector,
|
||||||
|
Scalar,
|
||||||
|
Selection,
|
||||||
|
Mesh,
|
||||||
|
Enum,
|
||||||
|
// The path to a (possibly new) file where export contents will be saved to
|
||||||
|
NewFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum InputParamMetadata {
|
||||||
|
MinMaxScalar { min: f32, max: f32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum InputParamValue {
|
||||||
|
Vector(Vec3<f32>),
|
||||||
|
Scalar(f32),
|
||||||
|
Selection {
|
||||||
|
text: String,
|
||||||
|
selection: Option<Vec<u32>>,
|
||||||
|
},
|
||||||
|
/// Used for parameters that can't have a value because they only accept
|
||||||
|
/// connections.
|
||||||
|
None,
|
||||||
|
Enum {
|
||||||
|
values: Vec<String>,
|
||||||
|
selection: Option<u32>,
|
||||||
|
},
|
||||||
|
NewFile {
|
||||||
|
path: Option<std::path::PathBuf>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// There are three kinds of input params
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub enum InputParamKind {
|
||||||
|
/// No constant value can be set. Only incoming connections can produce it
|
||||||
|
ConnectionOnly,
|
||||||
|
/// Only a constant value can be set. No incoming connections accepted.
|
||||||
|
ConstantOnly,
|
||||||
|
/// Both incoming connections and constants are accepted. Connections take
|
||||||
|
/// precedence over the constant values.
|
||||||
|
ConnectionOrConstant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct InputParam {
|
||||||
|
pub id: InputId,
|
||||||
|
/// The data type of this node. Used to determine incoming connections. This
|
||||||
|
/// should always match the type of the InputParamValue, but the property is
|
||||||
|
/// not actually enforced.
|
||||||
|
pub typ: DataType,
|
||||||
|
/// The constant value stored in this parameter.
|
||||||
|
pub value: InputParamValue,
|
||||||
|
/// A list of metadata fields, specifying things like bounds or limits.
|
||||||
|
/// Metadata values that don't make sense for a type are ignored.
|
||||||
|
pub metadata: SVec<InputParamMetadata>,
|
||||||
|
/// The input kind. See [InputParamKind]
|
||||||
|
pub kind: InputParamKind,
|
||||||
|
/// Back-reference to the node containing this parameter.
|
||||||
|
pub node: NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputParam {
|
||||||
|
pub fn value(&self) -> InputParamValue {
|
||||||
|
self.value.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> InputParamKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node(&self) -> NodeId {
|
||||||
|
self.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct OutputParam {
|
||||||
|
pub id: OutputId,
|
||||||
|
/// Back-reference to the node containing this parameter.
|
||||||
|
pub node: NodeId,
|
||||||
|
pub typ: DataType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputParam {
|
||||||
|
pub fn node(&self) -> NodeId {
|
||||||
|
self.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Node {
|
||||||
|
pub id: NodeId,
|
||||||
|
pub label: String,
|
||||||
|
pub op_name: String,
|
||||||
|
pub inputs: Vec<(String, InputId)>,
|
||||||
|
pub outputs: Vec<(String, OutputId)>,
|
||||||
|
/// Executable nodes will run some code when their "Run" button is clicked
|
||||||
|
pub is_executable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Graph {
|
||||||
|
pub nodes: SlotMap<NodeId, Node>,
|
||||||
|
pub inputs: SlotMap<InputId, InputParam>,
|
||||||
|
pub outputs: SlotMap<OutputId, OutputParam>,
|
||||||
|
// Connects the input of a node, to the output of its predecessor that
|
||||||
|
// produces it
|
||||||
|
pub connections: HashMap<InputId, OutputId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum InputDescriptor {
|
||||||
|
Vector { default: Vec3<f32> },
|
||||||
|
Mesh,
|
||||||
|
Selection,
|
||||||
|
Scalar { default: f32, min: f32, max: f32 },
|
||||||
|
Enum { values: Vec<String> },
|
||||||
|
NewFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OutputDescriptor(pub DataType);
|
||||||
|
|
||||||
|
pub struct NodeDescriptor {
|
||||||
|
pub op_name: String,
|
||||||
|
pub label: String,
|
||||||
|
pub inputs: Vec<(String, InputDescriptor)>,
|
||||||
|
pub outputs: Vec<(String, OutputDescriptor)>,
|
||||||
|
pub is_executable: bool,
|
||||||
|
}
|
24
src/id_types.rs
Normal file
24
src/id_types.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
slotmap::new_key_type! { pub struct NodeId; }
|
||||||
|
slotmap::new_key_type! { pub struct InputId; }
|
||||||
|
slotmap::new_key_type! { pub struct OutputId; }
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub enum AnyParameterId {
|
||||||
|
Input(InputId),
|
||||||
|
Output(OutputId),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnyParameterId {
|
||||||
|
pub fn assume_input(&self) -> InputId {
|
||||||
|
match self {
|
||||||
|
AnyParameterId::Input(input) => *input,
|
||||||
|
AnyParameterId::Output(output) => panic!("{:?} is not an InputId", output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn assume_output(&self) -> OutputId {
|
||||||
|
match self {
|
||||||
|
AnyParameterId::Output(output) => *output,
|
||||||
|
AnyParameterId::Input(input) => panic!("{:?} is not an OutputId", input),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
src/index_impls.rs
Normal file
36
src/index_impls.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::graph_types::*;
|
||||||
|
use crate::id_types::*;
|
||||||
|
|
||||||
|
macro_rules! impl_index_traits {
|
||||||
|
($id_type:ty, $output_type:ty, $arena:ident) => {
|
||||||
|
impl std::ops::Index<$id_type> for Graph {
|
||||||
|
type Output = $output_type;
|
||||||
|
|
||||||
|
fn index(&self, index: $id_type) -> &Self::Output {
|
||||||
|
self.$arena.get(index).unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"{} index error for {:?}. Has the value been deleted?",
|
||||||
|
stringify!($id_type),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::IndexMut<$id_type> for Graph {
|
||||||
|
fn index_mut(&mut self, index: $id_type) -> &mut Self::Output {
|
||||||
|
self.$arena.get_mut(index).unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"{} index error for {:?}. Has the value been deleted?",
|
||||||
|
stringify!($id_type),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_index_traits!(NodeId, Node, nodes);
|
||||||
|
impl_index_traits!(InputId, InputParam, inputs);
|
||||||
|
impl_index_traits!(OutputId, OutputParam, outputs);
|
41
src/main.rs
Normal file
41
src/main.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use editor_state::GraphEditorState;
|
||||||
|
|
||||||
|
mod graph_node_ui;
|
||||||
|
|
||||||
|
mod color_hex_utils;
|
||||||
|
mod editor_state;
|
||||||
|
mod graph_editor_egui;
|
||||||
|
mod graph_impls;
|
||||||
|
mod graph_types;
|
||||||
|
mod id_types;
|
||||||
|
mod index_impls;
|
||||||
|
mod node_finder;
|
||||||
|
mod node_types;
|
||||||
|
mod param_ui;
|
||||||
|
|
||||||
|
pub type SVec<T> = smallvec::SmallVec<[T; 4]>;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct App {
|
||||||
|
pub state: GraphEditorState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl epi::App for App {
|
||||||
|
fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
graph_editor_egui::draw_graph_editor(&ctx, &mut self.state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"Device Node Mapper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let options = eframe::NativeOptions {
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
eframe::run_native(Box::new(App::default()), options);
|
||||||
|
}
|
68
src/node_finder.rs
Normal file
68
src/node_finder.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use egui::*;
|
||||||
|
|
||||||
|
use crate::color_hex_utils::*;
|
||||||
|
use crate::node_types::GraphNodeType;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NodeFinder {
|
||||||
|
query: String,
|
||||||
|
/// Reset every frame. When set, the node finder will be moved at that position
|
||||||
|
pub position: Option<Pos2>,
|
||||||
|
pub just_spawned: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeFinder {
|
||||||
|
pub fn new_at(pos: Pos2) -> Self {
|
||||||
|
NodeFinder {
|
||||||
|
position: Some(pos),
|
||||||
|
just_spawned: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the node selector panel with a search bar. Returns whether a node
|
||||||
|
/// archetype was selected and, in that case, the finder should be hidden on
|
||||||
|
/// the next frame.
|
||||||
|
pub fn show(&mut self, ui: &mut Ui) -> Option<GraphNodeType> {
|
||||||
|
let background_color = color_from_hex("#3f3f3f").unwrap();
|
||||||
|
let _titlebar_color = background_color.linear_multiply(0.8);
|
||||||
|
let text_color = color_from_hex("#fefefe").unwrap();
|
||||||
|
|
||||||
|
ui.visuals_mut().widgets.noninteractive.fg_stroke = Stroke::new(2.0, text_color);
|
||||||
|
|
||||||
|
let frame = Frame::dark_canvas(ui.style())
|
||||||
|
.fill(background_color)
|
||||||
|
.margin(vec2(5.0, 5.0));
|
||||||
|
|
||||||
|
// The archetype that will be returned.
|
||||||
|
let mut submitted_archetype = None;
|
||||||
|
frame.show(ui, |ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
let resp = ui.text_edit_singleline(&mut self.query);
|
||||||
|
if self.just_spawned {
|
||||||
|
resp.request_focus();
|
||||||
|
self.just_spawned = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut query_submit = resp.lost_focus() && ui.input().key_down(Key::Enter);
|
||||||
|
|
||||||
|
Frame::default().margin(vec2(10.0, 10.0)).show(ui, |ui| {
|
||||||
|
for archetype in GraphNodeType::all_types() {
|
||||||
|
let archetype_name = archetype.type_label();
|
||||||
|
if archetype_name.contains(self.query.as_str()) {
|
||||||
|
if query_submit {
|
||||||
|
submitted_archetype = Some(archetype);
|
||||||
|
query_submit = false;
|
||||||
|
}
|
||||||
|
if ui.selectable_label(false, archetype_name).clicked() {
|
||||||
|
submitted_archetype = Some(archetype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
submitted_archetype
|
||||||
|
}
|
||||||
|
}
|
210
src/node_types.rs
Normal file
210
src/node_types.rs
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
use nalgebra::Vector3 as Vec3;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use crate::graph_types::*;
|
||||||
|
|
||||||
|
const ONE: Vec3<f32> = Vec3::new(1.0, 1.0, 1.0);
|
||||||
|
const ZERO: Vec3<f32> = Vec3::new(0.0, 0.0, 0.0);
|
||||||
|
const X: Vec3<f32> = Vec3::new(1.0, 0.0, 0.0);
|
||||||
|
const Y: Vec3<f32> = Vec3::new(0.0, 1.0, 0.0);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, strum_macros::EnumIter)]
|
||||||
|
pub enum GraphNodeType {
|
||||||
|
MakeBox,
|
||||||
|
MakeQuad,
|
||||||
|
BevelEdges,
|
||||||
|
ExtrudeFaces,
|
||||||
|
ChamferVertices,
|
||||||
|
MakeVector,
|
||||||
|
VectorMath,
|
||||||
|
MergeMeshes,
|
||||||
|
ExportObj,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! in_vector {
|
||||||
|
($name:expr, $default:expr) => {
|
||||||
|
(
|
||||||
|
$name.to_owned(),
|
||||||
|
InputDescriptor::Vector { default: $default },
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! in_scalar {
|
||||||
|
($name:expr, $default:expr, $min:expr, $max:expr) => {
|
||||||
|
(
|
||||||
|
$name.to_owned(),
|
||||||
|
InputDescriptor::Scalar {
|
||||||
|
default: $default,
|
||||||
|
max: $max,
|
||||||
|
min: $min,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($name:expr) => {
|
||||||
|
in_scalar!($name, 0.0, -1.0, 2.0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! in_mesh {
|
||||||
|
($name:expr) => {
|
||||||
|
($name.to_owned(), InputDescriptor::Mesh)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! out_mesh {
|
||||||
|
($name:expr) => {
|
||||||
|
($name.to_owned(), OutputDescriptor(DataType::Mesh))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! out_vector {
|
||||||
|
($name:expr) => {
|
||||||
|
($name.to_owned(), OutputDescriptor(DataType::Vector))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! in_selection {
|
||||||
|
($name:expr) => {
|
||||||
|
($name.to_owned(), InputDescriptor::Selection)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! in_file {
|
||||||
|
($name:expr) => {
|
||||||
|
($name.to_owned(), InputDescriptor::NewFile)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! in_enum {
|
||||||
|
($name:expr, $( $values:expr ),+) => {
|
||||||
|
($name.to_owned(), InputDescriptor::Enum { values: vec![$( $values.to_owned() ),+] })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphNodeType {
|
||||||
|
pub fn to_descriptor(&self) -> NodeDescriptor {
|
||||||
|
let label = self.type_label().into();
|
||||||
|
let op_name = self.op_name().into();
|
||||||
|
match self {
|
||||||
|
GraphNodeType::MakeBox => NodeDescriptor {
|
||||||
|
op_name,
|
||||||
|
label,
|
||||||
|
inputs: vec![in_vector!("origin", ZERO), in_vector!("size", ONE)],
|
||||||
|
outputs: vec![out_mesh!("out_mesh")],
|
||||||
|
is_executable: false,
|
||||||
|
},
|
||||||
|
GraphNodeType::MakeQuad => NodeDescriptor {
|
||||||
|
op_name,
|
||||||
|
label,
|
||||||
|
inputs: vec![
|
||||||
|
in_vector!("center", ZERO),
|
||||||
|
in_vector!("normal", Y),
|
||||||
|
in_vector!("right", X),
|
||||||
|
in_vector!("size", ONE),
|
||||||
|
],
|
||||||
|
outputs: vec![out_mesh!("out_mesh")],
|
||||||
|
is_executable: false,
|
||||||
|
},
|
||||||
|
GraphNodeType::BevelEdges => NodeDescriptor {
|
||||||
|
op_name,
|
||||||
|
label,
|
||||||
|
inputs: vec![
|
||||||
|
in_mesh!("in_mesh"),
|
||||||
|
in_selection!("edges"),
|
||||||
|
in_scalar!("amount", 0.0, 0.0, 1.0),
|
||||||
|
],
|
||||||
|
outputs: vec![out_mesh!("out_mesh")],
|
||||||
|
is_executable: false,
|
||||||
|
},
|
||||||
|
GraphNodeType::ExtrudeFaces => NodeDescriptor {
|
||||||
|
op_name,
|
||||||
|
label,
|
||||||
|
inputs: vec![
|
||||||
|
in_mesh!("in_mesh"),
|
||||||
|
in_selection!("faces"),
|
||||||
|
in_scalar!("amount", 0.0, 0.0, 1.0),
|
||||||
|
],
|
||||||
|
outputs: vec![out_mesh!("out_mesh")],
|
||||||
|
is_executable: false,
|
||||||
|
},
|
||||||
|
GraphNodeType::ChamferVertices => NodeDescriptor {
|
||||||
|
op_name,
|
||||||
|
label,
|
||||||
|
inputs: vec![
|
||||||
|
in_mesh!("in_mesh"),
|
||||||
|
in_selection!("vertices"),
|
||||||
|
in_scalar!("amount", 0.0, 0.0, 1.0),
|
||||||
|
],
|
||||||
|
outputs: vec![out_mesh!("out_mesh")],
|
||||||
|
is_executable: false,
|
||||||
|
},
|
||||||
|
GraphNodeType::MakeVector => NodeDescriptor {
|
||||||
|
op_name,
|
||||||
|
label,
|
||||||
|
inputs: vec![in_scalar!("x"), in_scalar!("y"), in_scalar!("z")],
|
||||||
|
outputs: vec![out_vector!("out_vec")],
|
||||||
|
is_executable: false,
|
||||||
|
},
|
||||||
|
GraphNodeType::VectorMath => NodeDescriptor {
|
||||||
|
op_name,
|
||||||
|
label,
|
||||||
|
inputs: vec![
|
||||||
|
in_enum!("vec_op", "ADD", "SUB"),
|
||||||
|
in_vector!("A", ZERO),
|
||||||
|
in_vector!("B", ZERO),
|
||||||
|
],
|
||||||
|
outputs: vec![out_vector!("out_vec")],
|
||||||
|
is_executable: false,
|
||||||
|
},
|
||||||
|
GraphNodeType::MergeMeshes => NodeDescriptor {
|
||||||
|
op_name,
|
||||||
|
label,
|
||||||
|
inputs: vec![in_mesh!("A"), in_mesh!("B")],
|
||||||
|
outputs: vec![out_mesh!("out_mesh")],
|
||||||
|
is_executable: false,
|
||||||
|
},
|
||||||
|
GraphNodeType::ExportObj => NodeDescriptor {
|
||||||
|
op_name,
|
||||||
|
label,
|
||||||
|
inputs: vec![in_mesh!("mesh"), in_file!("export_path")],
|
||||||
|
outputs: vec![],
|
||||||
|
is_executable: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn all_types() -> impl Iterator<Item = GraphNodeType> {
|
||||||
|
GraphNodeType::iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
GraphNodeType::MakeBox => "Box",
|
||||||
|
GraphNodeType::MakeQuad => "Quad",
|
||||||
|
GraphNodeType::BevelEdges => "Bevel edges",
|
||||||
|
GraphNodeType::ExtrudeFaces => "Extrude faces",
|
||||||
|
GraphNodeType::ChamferVertices => "Chamfer vertices",
|
||||||
|
GraphNodeType::MakeVector => "Vector",
|
||||||
|
GraphNodeType::VectorMath => "Vector math",
|
||||||
|
GraphNodeType::MergeMeshes => "Merge meshes",
|
||||||
|
GraphNodeType::ExportObj => "OBJ Export",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The op_name is used by the graph compiler in graph_compiler.rs to select
|
||||||
|
/// which PolyASM instructions to emit.
|
||||||
|
pub fn op_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
GraphNodeType::MakeBox => "MakeBox",
|
||||||
|
GraphNodeType::MakeQuad => "MakeQuad",
|
||||||
|
GraphNodeType::BevelEdges => "BevelEdges",
|
||||||
|
GraphNodeType::ExtrudeFaces => "ExtrudeFaces",
|
||||||
|
GraphNodeType::ChamferVertices => "ChamferVertices",
|
||||||
|
GraphNodeType::MakeVector => "MakeVector",
|
||||||
|
GraphNodeType::VectorMath => "VectorMath",
|
||||||
|
GraphNodeType::MergeMeshes => "MergeMeshes",
|
||||||
|
GraphNodeType::ExportObj => "ExportObj",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
src/param_ui.rs
Normal file
89
src/param_ui.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use egui::*;
|
||||||
|
|
||||||
|
use crate::graph_types::*;
|
||||||
|
|
||||||
|
impl InputParam {
|
||||||
|
pub fn value_widget(&mut self, name: &str, ui: &mut Ui) {
|
||||||
|
match &mut self.value {
|
||||||
|
InputParamValue::Vector(vector) => {
|
||||||
|
ui.label(name);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("x");
|
||||||
|
ui.add(egui::DragValue::new(&mut vector.x).speed(0.1));
|
||||||
|
ui.label("y");
|
||||||
|
ui.add(egui::DragValue::new(&mut vector.y).speed(0.1));
|
||||||
|
ui.label("z");
|
||||||
|
ui.add(egui::DragValue::new(&mut vector.z).speed(0.1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
InputParamValue::Scalar(scalar) => {
|
||||||
|
let mut min = f32::NEG_INFINITY;
|
||||||
|
let mut max = f32::INFINITY;
|
||||||
|
for metadata in &self.metadata {
|
||||||
|
match metadata {
|
||||||
|
InputParamMetadata::MinMaxScalar {
|
||||||
|
min: min_val,
|
||||||
|
max: max_val,
|
||||||
|
} => {
|
||||||
|
min = *min_val;
|
||||||
|
max = *max_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(name);
|
||||||
|
ui.add(Slider::new(scalar, min..=max));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
InputParamValue::Selection { text, selection } => {
|
||||||
|
if ui.text_edit_singleline(text).changed() {
|
||||||
|
*selection = text
|
||||||
|
.split(',')
|
||||||
|
.map(|x| {
|
||||||
|
x.parse::<u32>()
|
||||||
|
.map_err(|_| anyhow::anyhow!("Cannot parse number"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputParamValue::None => {
|
||||||
|
ui.label(name);
|
||||||
|
}
|
||||||
|
InputParamValue::Enum { values, selection } => {
|
||||||
|
let selected = if let Some(selection) = selection {
|
||||||
|
values[*selection as usize].clone()
|
||||||
|
} else {
|
||||||
|
"".to_owned()
|
||||||
|
};
|
||||||
|
ComboBox::from_label(name)
|
||||||
|
.selected_text(selected)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for (idx, value) in values.iter().enumerate() {
|
||||||
|
ui.selectable_value(selection, Some(idx as u32), value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
InputParamValue::NewFile { path } => {
|
||||||
|
ui.label(name);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Select").clicked() {
|
||||||
|
*path = rfd::FileDialog::new().save_file();
|
||||||
|
}
|
||||||
|
if let Some(ref path) = path {
|
||||||
|
ui.label(
|
||||||
|
path.clone()
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap_or_else(|_| "<Invalid string>".to_owned()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ui.label("No file selected");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue