use iced::widget::{checkbox, column, container, slider, text};
use iced::{Color, Element, Length, Padding, Point, Theme, Vector};
use iced_nodegraph::prelude::*;
fn main() -> iced::Result {
iced::application(App::default, App::update, App::view)
.title("iced_nodegraph - basic")
.theme(App::theme)
.run()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Port {
Number,
Bool,
}
impl Port {
fn color(self) -> Color {
match self {
Port::Number => Color::from_rgb(0.90, 0.55, 0.20), Port::Bool => Color::from_rgb(0.30, 0.75, 0.45), }
}
}
#[derive(Clone, Copy)]
enum Value {
Num(f32),
Bool(bool),
}
impl Value {
fn as_num(self) -> f32 {
match self {
Value::Num(n) => n,
Value::Bool(b) => b as i32 as f32,
}
}
fn as_bool(self) -> bool {
matches!(self, Value::Bool(true))
}
}
type Pin = PinRef<usize, usize>;
const VALUE: usize = 0; const SWITCH: usize = 1; const GT0: usize = 2; const AND: usize = 3; const LAMP: usize = 4;
struct App {
positions: Vec<Point>,
edges: Vec<(Pin, Pin)>,
value: f32,
switch: bool,
}
impl Default for App {
fn default() -> Self {
Self {
positions: vec![
Point::new(60.0, 80.0),
Point::new(60.0, 250.0),
Point::new(300.0, 90.0),
Point::new(520.0, 170.0),
Point::new(740.0, 195.0),
],
edges: vec![
(PinRef::new(VALUE, 0), PinRef::new(GT0, 0)),
(PinRef::new(GT0, 1), PinRef::new(AND, 0)),
(PinRef::new(SWITCH, 0), PinRef::new(AND, 1)),
(PinRef::new(AND, 2), PinRef::new(LAMP, 0)),
],
value: 0.5,
switch: true,
}
}
}
#[derive(Debug, Clone)]
enum Message {
Moved { delta: Vector, ids: Vec<usize> },
Connected { from: Pin, to: Pin },
Disconnected { from: Pin, to: Pin },
Value(f32),
Switch(bool),
}
fn edge_stroke(start: Port, end: Port) -> ColorQuad {
ColorQuad::arc(start.color(), end.color())
}
fn pin_style(
theme: &Theme,
pin: &PinInfo<'_, usize, Port>,
_other: Option<&PinInfo<'_, usize, Port>>,
status: PinStatus,
) -> PinStyle {
PinStyle {
color: pin.info().color().into(),
..default_pin_style(theme, status)
}
}
fn gate<'a>(
id: usize,
pos: Point,
body: impl Into<Element<'a, Message>>,
) -> Node<'a, usize, usize, Port, Message, Theme, iced::Renderer> {
node(id, pos, container(body).width(150.0)).pin_style(pin_style)
}
fn framed<'a>(
title: &'a str,
header_bg: Color,
body: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
let pad = Padding {
top: 4.0,
bottom: 4.0,
left: 8.0,
right: 8.0,
};
column![
node_header(container(text(title).size(13)).padding(pad), header_bg, 5.0),
container(body).width(Length::Fill).padding(pad),
]
.width(Length::Fill)
.into()
}
impl App {
fn theme(&self) -> Theme {
Theme::SolarizedLight
}
fn output(&self, node: usize, depth: u8) -> Option<Value> {
if depth == 0 {
return None;
}
Some(match node {
VALUE => Value::Num(self.value),
SWITCH => Value::Bool(self.switch),
GT0 => Value::Bool(self.input(GT0, 0, depth)?.as_num() > 0.0),
AND => Value::Bool(
self.input(AND, 0, depth)?.as_bool() && self.input(AND, 1, depth)?.as_bool(),
),
_ => return None,
})
}
fn input(&self, node: usize, pin: usize, depth: u8) -> Option<Value> {
let (from, _) = self
.edges
.iter()
.find(|(_, to)| to.node_id == node && to.pin_id == pin)?;
self.output(from.node_id, depth - 1)
}
fn update(&mut self, message: Message) {
match message {
Message::Moved { delta, ids } => {
for id in ids {
self.positions[id] += delta;
}
}
Message::Connected { from, to } => self.edges.push((from, to)),
Message::Disconnected { from, to } => self.edges.retain(|&e| e != (from, to)),
Message::Value(v) => self.value = v,
Message::Switch(b) => self.switch = b,
}
}
fn view(&self) -> Element<'_, Message> {
let theme = self.theme();
let p = &self.positions;
let pal = theme.extended_palette();
let input_bg = pal.primary.base.color;
let process_bg = pal.success.base.color;
let output_bg = pal.secondary.base.color;
let mut ng: NodeGraph<usize, usize, Port, Message, Theme, iced::Renderer> =
NodeGraph::default()
.on_move(|delta, ids| Message::Moved { delta, ids })
.on_connect(|from, to| Message::Connected { from, to })
.on_disconnect(|from, to| Message::Disconnected { from, to })
.can_connect(|from, to| {
from.direction() != to.direction() && from.info() == to.info()
})
.dragging_edge_style(|theme, pin| EdgeStyle {
stroke_color: edge_stroke(*pin.info(), *pin.info()),
..default_edge_style(theme, EdgeStatus::Idle)
});
ng.push_node(gate(
VALUE,
p[VALUE],
framed(
"Value",
input_bg,
column![
slider(-1.0..=1.0, self.value, Message::Value).step(0.01_f32),
text(format!("{:.2}", self.value)).size(11),
pin!(Right, 0usize, text("n"), Output, Port::Number),
]
.spacing(4),
),
));
ng.push_node(gate(
SWITCH,
p[SWITCH],
framed(
"Switch",
input_bg,
column![
checkbox(self.switch).on_toggle(Message::Switch),
pin!(Right, 0usize, text("b"), Output, Port::Bool),
]
.spacing(4),
),
));
ng.push_node(gate(
GT0,
p[GT0],
framed(
">0",
process_bg,
column![
pin!(Left, 0usize, text("x"), Input, Port::Number),
pin!(Right, 1usize, text("out"), Output, Port::Bool),
]
.spacing(4),
),
));
ng.push_node(gate(
AND,
p[AND],
framed(
"AND",
process_bg,
column![
pin!(Left, 0usize, text("a"), Input, Port::Bool),
pin!(Left, 1usize, text("b"), Input, Port::Bool),
pin!(Right, 2usize, text("out"), Output, Port::Bool),
]
.spacing(4),
),
));
let lit = self.input(LAMP, 0, 8).map(Value::as_bool).unwrap_or(false);
let lamp = if lit {
Color::from_rgb(0.95, 0.85, 0.20)
} else {
Color::from_rgb(0.40, 0.40, 0.40)
};
ng.push_node(gate(
LAMP,
p[LAMP],
framed(
"Lamp",
output_bg,
column![
pin!(Left, 0usize, text("in"), Input, Port::Bool),
text("\u{25CF}").size(22).color(lamp),
]
.spacing(4),
),
));
for &(from, to) in &self.edges {
ng.push_edge(
edge!(from, to).style(|theme, status, start, end| EdgeStyle {
stroke_color: edge_stroke(*start.info(), *end.info()),
..default_edge_style(theme, status)
}),
);
}
ng.into()
}
}