diff --git a/.gitignore b/.gitignore index 96ef6c0..98d325e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +dist/ diff --git a/egui_node_graph/Cargo.toml b/egui_node_graph/Cargo.toml index 25292d4..a52b5f4 100644 --- a/egui_node_graph/Cargo.toml +++ b/egui_node_graph/Cargo.toml @@ -15,7 +15,7 @@ workspace = ".." persistence = ["serde", "slotmap/serde", "smallvec/serde", "egui/persistence"] [dependencies] -egui = { version = "0.19.0" } +egui = { version = "0.20.0" } slotmap = { version = "1.0" } smallvec = { version = "1.7.0" } serde = { version = "1.0", optional = true, features = ["derive"] } diff --git a/egui_node_graph/src/editor_ui.rs b/egui_node_graph/src/editor_ui.rs index dbc61a8..756bfd7 100644 --- a/egui_node_graph/src/editor_ui.rs +++ b/egui_node_graph/src/editor_ui.rs @@ -5,6 +5,7 @@ use crate::utils::ColorUtils; use super::*; use egui::epaint::{CubicBezierShape, RectShape}; +use egui::style::Margin; use egui::*; pub type PortLocations = std::collections::HashMap; @@ -408,9 +409,9 @@ where self.node_finder = None; } - if r.dragged() && ui.ctx().input().pointer.middle_down() { - self.pan_zoom.pan += ui.ctx().input().pointer.delta(); - } + // if r.dragged() && ui.ctx().input().pointer.middle_down() { + // self.pan_zoom.pan += ui.ctx().input().pointer.delta(); + // } // Deselect and deactivate finder if the editor backround is clicked, // *or* if the the mouse clicks off the ui @@ -488,7 +489,12 @@ where ui: &mut Ui, user_state: &mut UserState, ) -> Vec> { - let margin = egui::vec2(15.0, 5.0); + let margin = Margin { + left: 15.0, + right: 15.0, + top: 5.0, + bottom: 15.0, + }; let mut responses = Vec::>::new(); let background_color; @@ -507,82 +513,76 @@ where let outline_shape = ui.painter().add(Shape::Noop); 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(&self.graph[self.node_id].label) - .text_style(TextStyle::Button) - .color(text_color), - )); - ui.add_space(8.0); // The size of the little cross icon - }); - ui.add_space(margin.y); - title_height = ui.min_size().y; + Frame::none().inner_margin(margin).show(ui, |ui| { + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.add(Label::new( + RichText::new(&self.graph[self.node_id].label) + .text_style(TextStyle::Button) + .color(text_color), + )); + ui.add_space(8.0); // The size of the little cross icon + }); + title_height = ui.min_size().y; + ui.add_space(margin.top); + + // First pass: Draw the inner fields. Compute port heights + let inputs = self.graph[self.node_id].inputs.clone(); + for (param_name, param_id) in inputs { + if self.graph[param_id].shown_inline { + let height_before = ui.min_rect().bottom(); + if self.graph.connection(param_id).is_some() { + ui.label(param_name); + } else { + // NOTE: We want to pass the `user_data` to + // `value_widget`, but we can't since that would require + // borrowing the graph twice. Here, we make the + // assumption that the value is cheaply replaced, and + // use `std::mem::take` to temporarily replace it with a + // dummy value. This requires `ValueType` to implement + // Default, but results in a totally safe alternative. + let mut value = std::mem::take(&mut self.graph[param_id].value); + let node_responses = value.value_widget( + ¶m_name, + self.node_id, + ui, + user_state, + &self.graph[self.node_id].user_data, + ); + self.graph[param_id].value = value; + responses.extend(node_responses.into_iter().map(NodeResponse::User)); + } + let height_after = ui.min_rect().bottom(); + input_port_heights.push((height_before + height_after) / 2.0); + } + } - // First pass: Draw the inner fields. Compute port heights - let inputs = self.graph[self.node_id].inputs.clone(); - for (param_name, param_id) in inputs { - if self.graph[param_id].shown_inline { + let outputs = self.graph[self.node_id].outputs.clone(); + for (param_name, _param) in outputs { let height_before = ui.min_rect().bottom(); - if self.graph.connection(param_id).is_some() { - ui.label(param_name); - } else { - // NOTE: We want to pass the `user_data` to - // `value_widget`, but we can't since that would require - // borrowing the graph twice. Here, we make the - // assumption that the value is cheaply replaced, and - // use `std::mem::take` to temporarily replace it with a - // dummy value. This requires `ValueType` to implement - // Default, but results in a totally safe alternative. - let mut value = std::mem::take(&mut self.graph[param_id].value); - let node_responses = value.value_widget( - ¶m_name, - self.node_id, - ui, - user_state, - &self.graph[self.node_id].user_data, - ); - self.graph[param_id].value = value; - responses.extend(node_responses.into_iter().map(NodeResponse::User)); - } + ui.label(¶m_name); let height_after = ui.min_rect().bottom(); - input_port_heights.push((height_before + height_after) / 2.0); + output_port_heights.push((height_before + height_after) / 2.0); } - } - - let outputs = self.graph[self.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); - } - responses.extend( - self.graph[self.node_id] - .user_data - .bottom_ui(ui, self.node_id, self.graph, user_state) - .into_iter(), - ); + responses.extend( + self.graph[self.node_id] + .user_data + .bottom_ui(ui, self.node_id, self.graph, user_state) + .into_iter(), + ); + }) }); // 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 outer_rect = ui.min_rect(); let port_left = outer_rect.left(); let port_right = outer_rect.right(); @@ -628,7 +628,7 @@ where port_type.data_type_color(user_state) }; ui.painter() - .circle(port_rect.center(), 5.0, port_color, Stroke::none()); + .circle(port_rect.center(), 5.0, port_color, Stroke::NONE); if resp.drag_started() { if is_connected_input { @@ -722,50 +722,48 @@ where let (shape, outline) = { let rounding_radius = 4.0; - let rounding = Rounding::same(rounding_radius); - let titlebar_height = title_height + margin.y; + let titlebar_height = title_height + margin.top * 2.0; let titlebar_rect = Rect::from_min_size(outer_rect.min, vec2(outer_rect.width(), titlebar_height)); let titlebar = Shape::Rect(RectShape { rect: titlebar_rect, - rounding, + rounding: Rounding { + nw: rounding_radius, + ne: rounding_radius, + sw: 0.0, + se: 0.0, + }, fill: self.graph[self.node_id] .user_data .titlebar_color(ui, self.node_id, self.graph, user_state) .unwrap_or_else(|| background_color.lighten(0.8)), - stroke: Stroke::none(), + stroke: Stroke::NONE, }); let body_rect = Rect::from_min_size( - outer_rect.min + vec2(0.0, titlebar_height - rounding_radius), + outer_rect.min + vec2(0.0, titlebar_height), vec2(outer_rect.width(), outer_rect.height() - titlebar_height), ); let body = Shape::Rect(RectShape { rect: body_rect, - rounding: Rounding::none(), - 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, - rounding, + rounding: Rounding { + nw: 0.0, + ne: 0.0, + sw: rounding_radius, + se: rounding_radius, + }, fill: background_color, - stroke: Stroke::none(), + stroke: Stroke::NONE, }); - let node_rect = titlebar_rect.union(body_rect).union(bottom_body_rect); + let node_rect = titlebar_rect.union(body_rect); let outline = if self.selected { Shape::Rect(RectShape { rect: node_rect.expand(1.0), - rounding, + rounding: Rounding::same(rounding_radius), fill: Color32::WHITE.lighten(0.8), - stroke: Stroke::none(), + stroke: Stroke::NONE, }) } else { Shape::Noop @@ -774,7 +772,7 @@ where // Take note of the node rect, so the editor can use it later to compute intersections. self.node_rects.insert(self.node_id, node_rect); - (Shape::Vec(vec![titlebar, body, bottom_body]), outline) + (Shape::Vec(vec![titlebar, body]), outline) }; ui.painter().set(background_shape, shape); diff --git a/egui_node_graph_example/Cargo.toml b/egui_node_graph_example/Cargo.toml index 7748d93..ccf4b3e 100644 --- a/egui_node_graph_example/Cargo.toml +++ b/egui_node_graph_example/Cargo.toml @@ -8,15 +8,21 @@ rust-version = "1.56" [lib] crate-type = ["cdylib", "rlib"] +[features] +default = [] +persistence = ["serde", "egui_node_graph/persistence", "eframe/persistence"] + [dependencies] -eframe = "0.19.0" +eframe = "0.20.0" egui_node_graph = { path = "../egui_node_graph" } anyhow = "1.0" serde = { version = "1.0", optional = true } -[features] -default = [] -persistence = ["serde", "egui_node_graph/persistence", "eframe/persistence"] +# web: +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" +console_error_panic_hook = "0.1.6" +tracing-wasm = "0.2" [profile.release] opt-level = 2 # fast and small wasm diff --git a/egui_node_graph_example/Trunk.toml b/egui_node_graph_example/Trunk.toml new file mode 100644 index 0000000..bd6c484 --- /dev/null +++ b/egui_node_graph_example/Trunk.toml @@ -0,0 +1,2 @@ +[build] +filehash = false diff --git a/egui_node_graph_example/assets/favicon.ico b/egui_node_graph_example/assets/favicon.ico new file mode 100755 index 0000000..61ad031 Binary files /dev/null and b/egui_node_graph_example/assets/favicon.ico differ diff --git a/egui_node_graph_example/assets/icon-1024.png b/egui_node_graph_example/assets/icon-1024.png new file mode 100644 index 0000000..1b5868a Binary files /dev/null and b/egui_node_graph_example/assets/icon-1024.png differ diff --git a/egui_node_graph_example/assets/icon-256.png b/egui_node_graph_example/assets/icon-256.png new file mode 100644 index 0000000..ae72287 Binary files /dev/null and b/egui_node_graph_example/assets/icon-256.png differ diff --git a/egui_node_graph_example/assets/icon_ios_touch_192.png b/egui_node_graph_example/assets/icon_ios_touch_192.png new file mode 100644 index 0000000..8472802 Binary files /dev/null and b/egui_node_graph_example/assets/icon_ios_touch_192.png differ diff --git a/egui_node_graph_example/assets/manifest.json b/egui_node_graph_example/assets/manifest.json new file mode 100644 index 0000000..2a137fb --- /dev/null +++ b/egui_node_graph_example/assets/manifest.json @@ -0,0 +1,28 @@ +{ + "name": "egui Template PWA", + "short_name": "egui-template-pwa", + "icons": [ + { + "src": "./icon-256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "./maskable_icon_x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "./icon-1024.png", + "sizes": "1024x1024", + "type": "image/png" + } + ], + "lang": "en-US", + "id": "/index.html", + "start_url": "./index.html", + "display": "standalone", + "background_color": "white", + "theme_color": "white" +} diff --git a/egui_node_graph_example/assets/maskable_icon_x512.png b/egui_node_graph_example/assets/maskable_icon_x512.png new file mode 100644 index 0000000..db8df3e Binary files /dev/null and b/egui_node_graph_example/assets/maskable_icon_x512.png differ diff --git a/egui_node_graph_example/assets/sw.js b/egui_node_graph_example/assets/sw.js new file mode 100644 index 0000000..d3038d7 --- /dev/null +++ b/egui_node_graph_example/assets/sw.js @@ -0,0 +1,25 @@ +var cacheName = 'egui-template-pwa'; +var filesToCache = [ + './', + './index.html', + './egui_node_graph_example.js', + './egui_node_graph_example_bg.wasm', +]; + +/* Start the service worker and cache all of the app's content */ +self.addEventListener('install', function (e) { + e.waitUntil( + caches.open(cacheName).then(function (cache) { + return cache.addAll(filesToCache); + }) + ); +}); + +/* Serve cached content when offline */ +self.addEventListener('fetch', function (e) { + e.respondWith( + caches.match(e.request).then(function (response) { + return response || fetch(e.request); + }) + ); +}); diff --git a/egui_node_graph_example/build_web.bat b/egui_node_graph_example/build_web.bat deleted file mode 100644 index 897f067..0000000 --- a/egui_node_graph_example/build_web.bat +++ /dev/null @@ -1,78 +0,0 @@ -@echo off - -SET script_path=%~dp0 -cd %script_path% - -SET OPEN=0 -SET FAST=0 - -:do_while - IF (%1) == () GOTO end_while - - IF %1 == -h GOTO print_help - IF %1 == --help GOTO print_help - - IF %1 == --fast ( - SET FAST=1 - SHIFT - GOTO do_while - ) - - IF %1 == --open ( - SET OPEN=1 - SHIFT - GOTO do_while - ) - - echo Unknown command "%1" -:end_while - -@REM call this first : `./setup_web.bat` - -for %%I in (.) do SET FOLDER_NAME=%%~nxI - -@REM assume crate name is the same as the folder name -SET CRATE_NAME=%FOLDER_NAME% - -@REM for those who name crates with-kebab-case -SET CRATE_NAME_SNAKE_CASE=%FOLDER_NAME:-=_% - -@REM This is required to enable the web_sys clipboard API which egui_web uses -@REM https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html -@REM https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html -SET RUSTFLAGS=--cfg=web_sys_unstable_apis - -@REM Clear output from old stuff: -DEL /F docs\%CRATE_NAME_SNAKE_CASE%_bg.wasm - -echo Building rust... -SET BUILD=release -cargo build -p %CRATE_NAME% --release --lib --target wasm32-unknown-unknown - -@REM Get the output directory (in the workspace it is in another location) -FOR /F %%i IN ('cargo metadata --format-version=1 ^| jq --raw-output .target_directory') DO SET TARGET=%%i - -echo Generating JS bindings for wasm... -SET TARGET_NAME=%CRATE_NAME_SNAKE_CASE%.wasm -wasm-bindgen "%TARGET%\wasm32-unknown-unknown\%BUILD%\%TARGET_NAME%" --out-dir "docs" --no-modules --no-typescript - -IF %FAST% == 0 ( - echo Optimizing wasm... - @REM to get wasm-opt: apt/brew/dnf install binaryen - @REM add -g to get debug symbols : - wasm-opt "docs\%CRATE_NAME%_bg.wasm" -O2 --fast-math -o "docs\%CRATE_NAME%_bg.wasm" -) - -echo Finished: docs/%CRATE_NAME_SNAKE_CASE%.wasm" - -IF %OPEN% == 1 start http://localhost:8080/index.html - -GOTO end_program - -:print_help -echo build_web.sh [--fast] [--open] -echo --fast: skip optimization step -echo --open: open the result in a browser -GOTO end_program - -:end_program diff --git a/egui_node_graph_example/build_web.sh b/egui_node_graph_example/build_web.sh deleted file mode 100755 index 0de31a3..0000000 --- a/egui_node_graph_example/build_web.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash -set -eu -script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) -cd "$script_path" - -OPEN=false -FAST=false - -while test $# -gt 0; do - case "$1" in - -h|--help) - echo "build_web.sh [--fast] [--open]" - echo " --fast: skip optimization step" - echo " --open: open the result in a browser" - exit 0 - ;; - --fast) - shift - FAST=true - ;; - --open) - shift - OPEN=true - ;; - *) - break - ;; - esac -done - -# ./setup_web.sh # <- call this first! - -FOLDER_NAME=${PWD##*/} -CRATE_NAME=$FOLDER_NAME # assume crate name is the same as the folder name -CRATE_NAME_SNAKE_CASE="${CRATE_NAME//-/_}" # for those who name crates with-kebab-case - -# This is required to enable the web_sys clipboard API which egui_web uses -# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html -# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html -export RUSTFLAGS=--cfg=web_sys_unstable_apis - -# Clear output from old stuff: -rm -f "docs/${CRATE_NAME_SNAKE_CASE}_bg.wasm" - -echo "Building rust…" -BUILD=release -cargo build -p "${CRATE_NAME}" --release --lib --target wasm32-unknown-unknown - -# Get the output directory (in the workspace it is in another location) -TARGET=$(cargo metadata --format-version=1 | jq --raw-output .target_directory) - -echo "Generating JS bindings for wasm…" -TARGET_NAME="${CRATE_NAME_SNAKE_CASE}.wasm" -wasm-bindgen "${TARGET}/wasm32-unknown-unknown/${BUILD}/${TARGET_NAME}" \ - --out-dir docs --no-modules --no-typescript - -if [[ "${FAST}" == false ]]; then - echo "Optimizing wasm…" - # to get wasm-opt: apt/brew/dnf install binaryen - wasm-opt "docs/${CRATE_NAME}_bg.wasm" -O2 --fast-math -o "docs/${CRATE_NAME}_bg.wasm" # add -g to get debug symbols -fi - -echo "Finished: docs/${CRATE_NAME_SNAKE_CASE}.wasm" - -if [[ "${OPEN}" == true ]]; then - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - # Linux, ex: Fedora - xdg-open http://localhost:8080/index.html - elif [[ "$OSTYPE" == "msys" ]]; then - # Windows - start http://localhost:8080/index.html - else - # Darwin/MacOS, or something else - open http://localhost:8080/index.html - fi -fi diff --git a/egui_node_graph_example/docs/index.html b/egui_node_graph_example/docs/index.html index 61ea670..f463812 100644 --- a/egui_node_graph_example/docs/index.html +++ b/egui_node_graph_example/docs/index.html @@ -125,13 +125,13 @@ - + + + + + + diff --git a/egui_node_graph_example/setup_web.bat b/egui_node_graph_example/setup_web.bat deleted file mode 100644 index 5545ec6..0000000 --- a/egui_node_graph_example/setup_web.bat +++ /dev/null @@ -1,7 +0,0 @@ -@REM Pre-requisites: -rustup target add wasm32-unknown-unknown -cargo install wasm-bindgen-cli -cargo update -p wasm-bindgen - -@REM For local tests with `./start_server`: -cargo install basic-http-server diff --git a/egui_node_graph_example/setup_web.sh b/egui_node_graph_example/setup_web.sh deleted file mode 100755 index 8d5b5b2..0000000 --- a/egui_node_graph_example/setup_web.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -eu - -# Pre-requisites: -rustup target add wasm32-unknown-unknown -cargo install wasm-bindgen-cli -cargo update -p wasm-bindgen - -# For local tests with `./start_server`: -cargo install basic-http-server diff --git a/egui_node_graph_example/src/lib.rs b/egui_node_graph_example/src/lib.rs index b2faa05..b7adae3 100644 --- a/egui_node_graph_example/src/lib.rs +++ b/egui_node_graph_example/src/lib.rs @@ -4,20 +4,3 @@ mod app; pub use app::NodeGraphExample; - -// ---------------------------------------------------------------------------- -// When compiling for web: - -#[cfg(target_arch = "wasm32")] -use eframe::wasm_bindgen::{self, prelude::*}; - -/// This is the entry-point for all the web-assembly. -/// This is called once from the HTML. -/// It loads the app, installs some callbacks, then returns. -/// You can add more callbacks like this if you want to call in to your code. -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen] -pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> { - let app = NodeGraphExample::default(); - eframe::start_web(canvas_id, Box::new(app)) -} diff --git a/egui_node_graph_example/src/main.rs b/egui_node_graph_example/src/main.rs index 02d1528..d40aa9e 100644 --- a/egui_node_graph_example/src/main.rs +++ b/egui_node_graph_example/src/main.rs @@ -2,11 +2,10 @@ #![cfg_attr(not(debug_assertions), deny(warnings))] // Forbid warnings in release builds #![warn(clippy::all, rust_2018_idioms)] +use eframe::egui::Visuals; // When compiling natively: #[cfg(not(target_arch = "wasm32"))] fn main() { - use eframe::egui::Visuals; - eframe::run_native( "Egui node graph example", eframe::NativeOptions::default(), @@ -21,3 +20,34 @@ fn main() { }), ); } + +// when compiling to web using trunk. + +#[cfg(target_arch = "wasm32")] +fn main() { + // Make sure panics are logged using `console.error`. + console_error_panic_hook::set_once(); + + // Redirect tracing to console.log and friends: + tracing_wasm::set_as_global_default(); + + let web_options = eframe::WebOptions::default(); + + wasm_bindgen_futures::spawn_local(async { + eframe::start_web( + "the_canvas_id", // hardcode it + web_options, + Box::new(|cc| { + cc.egui_ctx.set_visuals(Visuals::dark()); + #[cfg(feature = "persistence")] + { + Box::new(egui_node_graph_example::NodeGraphExample::new(cc)) + } + #[cfg(not(feature = "persistence"))] + Box::new(egui_node_graph_example::NodeGraphExample::default()) + }), + ) + .await + .expect("failed to start eframe"); + }); +} diff --git a/egui_node_graph_example/start_server.bat b/egui_node_graph_example/start_server.bat deleted file mode 100644 index 0fb9d97..0000000 --- a/egui_node_graph_example/start_server.bat +++ /dev/null @@ -1,11 +0,0 @@ -@echo off - -@REM Starts a local web-server that serves the contents of the `doc/` folder, -@REM which is the folder to where the web version is compiled. - -cargo install basic-http-server - -echo "open http://localhost:8080" - -(cd docs && basic-http-server --addr 127.0.0.1:8080 .) -@REM (cd docs && python3 -m http.server 8080 --bind 127.0.0.1) diff --git a/egui_node_graph_example/start_server.sh b/egui_node_graph_example/start_server.sh deleted file mode 100755 index b586103..0000000 --- a/egui_node_graph_example/start_server.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -eu - -# Starts a local web-server that serves the contents of the `doc/` folder, -# which is the folder to where the web version is compiled. - -cargo install basic-http-server - -echo "open http://localhost:8080" - -(cd docs && basic-http-server --addr 127.0.0.1:8080 .) -# (cd docs && python3 -m http.server 8080 --bind 127.0.0.1)