JWT Authentication

This commit is contained in:
Michael Mikovsky
2025-11-29 13:15:09 -07:00
parent fcb8c6f6f5
commit a10bdce38f
18 changed files with 1198 additions and 583 deletions
+100
View File
@@ -75,6 +75,15 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "arboard"
version = "3.6.1"
@@ -292,6 +301,19 @@ dependencies = [
"libc",
]
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
@@ -952,6 +974,30 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.1.1"
@@ -2353,6 +2399,7 @@ checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
name = "unshell-gui"
version = "0.1.0"
dependencies = [
"chrono",
"eframe",
"egui",
"egui_extras",
@@ -2748,12 +2795,65 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
+1
View File
@@ -40,6 +40,7 @@ wasm-bindgen-futures = "0.4.50"
wasm-bindgen = "0.2.106"
web-sys = "0.3.70" # to access the DOM (to hide the loading text)
serde_json = "1.0.145"
chrono = "0.4.42"
[profile.release]
opt-level = 2 # fast and small wasm
+26 -15
View File
@@ -31,34 +31,45 @@ self.addEventListener("fetch", function (e) {
// return xmlHttp.responseText;
// }
export function httpGet(theUrl, callback) {
function startHttpRequest(callback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState !== 4) return;
if (xmlHttp.status == 200) callback(xmlHttp.responseText);
else alert("Error " + xmlHttp.status + ", " + xmlHttp.responseText);
if (xmlHttp.status == 200) callback(true, xmlHttp.responseText);
else callback(false, xmlHttp.responseText);
};
return xmlHttp;
}
export function httpGet(theUrl, callback) {
var xmlHttp = startHttpRequest(callback);
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.send(null);
}
export function httpPost(url, body, callback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState !== 4) return;
if (xmlHttp.status === 200) {
// var json = JSON.parse(xhr.responseText);
callback(xmlHttp.responseText);
} else {
alert("Error " + xmlHttp.status + ", " + xmlHttp.responseText);
}
};
var xmlHttp = startHttpRequest(callback);
xmlHttp.open("POST", url, true);
xmlHttp.setRequestHeader("Content-Type", "application/json");
// var data = JSON.stringify({ email: "[email protected]", password: "101010" });
xmlHttp.send(body);
}
export function httpGetAuth(theUrl, auth, callback) {
var xmlHttp = startHttpRequest(callback);
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.setRequestHeader("authorization", auth);
xmlHttp.send(null);
}
export function httpPostAuth(url, auth, body, callback) {
var xmlHttp = startHttpRequest(callback);
xmlHttp.open("POST", url, true);
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.setRequestHeader("authorization", auth);
// var data = JSON.stringify({ email: "[email protected]", password: "101010" });
xmlHttp.send(body);
}
+3 -8
View File
@@ -1,16 +1,12 @@
use egui::Frame;
use egui_tiles::Tree;
use crate::{
app::{AppState, windows::WindowWrapper},
auth::Auth,
};
use crate::app::{AppState, windows::WindowWrapper};
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)] // if we add new fields, give them default values when deserializing old state
pub struct TemplateApp {
auth: Auth,
// tab: Tab,
state: AppState,
tree: Tree<WindowWrapper>,
@@ -19,7 +15,6 @@ pub struct TemplateApp {
impl Default for TemplateApp {
fn default() -> Self {
Self {
auth: Auth::default(),
state: AppState::default(),
tree: egui_tiles::Tree::new_horizontal("tree_root", Vec::new()),
}
@@ -50,10 +45,10 @@ impl eframe::App for TemplateApp {
/// Called each time the UI needs repainting, which may be many times per second.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if !self.auth.logged_in() {
if !self.state.auth.logged_in() {
egui::CentralPanel::default()
.frame(Frame::central_panel(&ctx.style()).inner_margin(0))
.show(ctx, |ui| self.auth.update(ui));
.show(ctx, |ui| self.state.auth.update(ui));
} else {
egui::TopBottomPanel::top("tab_panel").show(ctx, |ui| {
// The top panel is often a good place for a menu bar:
+6 -4
View File
@@ -3,12 +3,14 @@ mod windows;
use std::collections::HashMap;
use crate::{app::windows::WindowWrapper, config::Config, flowchart::FlowChart};
use crate::{app::windows::WindowWrapper, auth::Auth, config::Config, flowchart::FlowChart};
pub use app::TemplateApp;
use egui_tiles::{TileId, Tree};
#[derive(Default, serde::Deserialize, serde::Serialize)]
struct AppState {
pub struct AppState {
pub auth: Auth,
pub open_windows: HashMap<AppWindow, TileId>,
pub flowchart: FlowChart,
@@ -64,7 +66,7 @@ impl AppState {
}
#[derive(Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq, Eq, Hash)]
enum AppWindow {
pub enum AppWindow {
Flowchart,
Config,
}
@@ -73,7 +75,7 @@ impl AppWindow {
fn update(&self, state: &mut AppState, ui: &mut egui::Ui) {
match self {
AppWindow::Flowchart => state.flowchart.paint(ui),
AppWindow::Config => state.config.update(ui),
AppWindow::Config => state.config.update(&mut state.auth, ui),
}
}
}
+81 -39
View File
@@ -1,5 +1,7 @@
use std::{collections::HashMap, sync::Arc};
use serde_json::json;
use std::{collections::HashMap, sync::Arc, time::Duration};
use chrono::Utc;
use egui::{Align2, Area, Frame, Order, Sense, UiKind, Vec2, mutex::Mutex};
use wasm_bindgen::prelude::Closure;
@@ -12,18 +14,20 @@ pub struct Auth {
// UI Stuff
username: String,
#[serde(skip)]
password: String,
show_password: bool,
}
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
struct Token {
access_token: String,
token_type: String,
expiration: u128,
token: String,
}
#[derive(Debug, PartialEq, Eq)]
enum AuthState {
Unset,
NotLoggedIn,
RequestSent,
Authorised(Token),
@@ -32,25 +36,40 @@ enum AuthState {
impl Default for AuthState {
fn default() -> Self {
Self::NotLoggedIn
Self::Unset
}
}
impl Auth {
/// Refresh the authentication state
pub fn logged_in(&mut self) -> bool {
if self.token.is_some() {
true
} else {
match *self.auth_state.lock() {
AuthState::Authorised(ref token) => {
self.token = Some(token.clone());
true
}
_ => false,
}
}
match (self.token.is_some(), &*self.auth_state.lock()) {
// The client is actually authorized
(true, AuthState::Authorised(_)) => true,
// self.auth_state.lock().eq(&AuthState::Authorised)
// If the user has just reloaded the session,
// the AuthState is not automatically set by any other process
(true, AuthState::Unset) => true,
// If the authentication state has been updated to unauthorized, delete the token
(true, _) => {
self.token = None;
false
}
// If the authentication state has been updated to authorized, set the token
(false, AuthState::Authorised(token)) => {
self.token = Some(token.clone());
// Also clear the password because it is bad to store it
self.password.clear();
true
}
// The client is actually unauthorized
(false, _) => false,
}
}
pub fn update(&mut self, ui: &mut egui::Ui) {
@@ -82,38 +101,61 @@ impl Auth {
// ui.toggle_value(&mut self.show_password, "Show");
});
if ui.button("Login").clicked() {
let json = serde_json::to_string(&HashMap::from([
("client_id", self.username.clone()),
("client_secret", self.password.clone()),
]))
.unwrap();
ui.horizontal(|ui| {
if ui.button("Login").clicked() {
// Try to
ui.ctx().request_repaint_after(Duration::from_millis(500));
let state = self.auth_state.clone();
*(state.lock()) = AuthState::RequestSent;
let state = self.auth_state.clone();
crate::httpPost(
"/auth",
&json,
Closure::once_into_js(move |response: String| {
*(state.lock()) =
if let Ok(token) = serde_json::from_str::<Token>(&response) {
AuthState::Authorised(token)
crate::httpPost(
"/auth",
&json!({
"username": self.username.clone(),
"password": self.password.clone()
})
.to_string(),
Closure::once_into_js(move |ok: bool, response: String| {
*(state.lock()) = if ok {
if let Ok(token) = serde_json::from_str::<Token>(&response)
{
AuthState::Authorised(token)
} else {
AuthState::Error("Malformed Response".into())
}
} else {
AuthState::Error("Malformed Response".into())
AuthState::Error(response)
}
}),
);
// self.logged_in
// .store(true, std::sync::atomic::Ordering::Relaxed);
// self.logged_in();
}),
);
}
*(self.auth_state.lock()) = AuthState::RequestSent;
}
ui.label(format!("{:?}", self.auth_state.lock()));
});
});
});
// });
// });
}
pub fn test(&self) {
if let Some(ref token) = self.token {
let state = self.auth_state.clone();
crate::httpGetAuth(
"/api/test1234/kjhejwer/kwherjwer/iuwehrhiwer/wiuerhjwer",
format!("Bearer {}", token.token),
Closure::once_into_js(move |ok: bool, response: String| {
if ok {
crate::log(&response);
} else {
*(state.lock()) = AuthState::Error(response);
}
}),
);
}
}
}
impl Default for Auth {
+6 -224
View File
@@ -1,230 +1,12 @@
use egui::{TextStyle, Ui};
use egui_extras::{Column, TableBuilder};
use crate::auth::Auth;
#[derive(serde::Deserialize, serde::Serialize)]
pub struct Config {
state: ConfigState,
current_payloads: Vec<PayloadConfig>,
}
#[derive(serde::Deserialize, serde::Serialize)]
enum ConfigState {
Base,
NewConfig(PayloadConfig),
EditConfig(usize, PayloadConfig),
}
impl Default for Config {
fn default() -> Self {
Self {
state: ConfigState::Base,
current_payloads: vec![PayloadConfig {
name: "Test".to_string(),
components: vec!["server".to_string()],
// runtimes: Vec::new(),
}],
}
}
}
#[derive(Default, serde::Deserialize, serde::Serialize)]
pub struct Config {}
impl Config {
// pub fn title(&self) -> &str {
// match self.state {
// ConfigState::Base => "Config",
// ConfigState::NewConfig(..) => "Config/New",
// ConfigState::EditConfig(..) => "Config/Edit",
// }
// }
pub fn update(&mut self, ui: &mut Ui) {
match &mut self.state {
ConfigState::Base => self.table_ui(ui),
ConfigState::EditConfig(_, config) => {
ui.heading("Edit Payload");
match Self::edit_ui(ui, config) {
Some(true) => {
if let ConfigState::EditConfig(n, config) =
std::mem::replace(&mut self.state, ConfigState::Base)
{
self.current_payloads[n] = config;
}
}
Some(false) => {
self.state = ConfigState::Base;
}
_ => {}
}
}
ConfigState::NewConfig(config) => {
ui.heading("Edit Payload");
match Self::edit_ui(ui, config) {
Some(true) => {
if let ConfigState::NewConfig(config) =
std::mem::replace(&mut self.state, ConfigState::Base)
{
self.current_payloads.push(config);
}
}
Some(false) => {
self.state = ConfigState::Base;
}
_ => {}
}
}
}
}
fn table_ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
ui.heading("Payloads");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("New").clicked() {
self.state = ConfigState::NewConfig(PayloadConfig::new());
}
});
});
let body_text_size = TextStyle::Body.resolve(ui.style()).size;
use egui_extras::{Size, StripBuilder};
StripBuilder::new(ui)
.size(Size::remainder().at_least(100.0)) // for the table
.size(Size::exact(body_text_size)) // for the source code link
.vertical(|mut strip| {
strip.cell(|ui| {
egui::ScrollArea::horizontal().show(ui, |ui| {
let table = TableBuilder::new(ui)
.striped(true)
.resizable(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Column::auto().resizable(false))
.column(Column::remainder())
.column(Column::remainder())
.column(Column::remainder())
.column(Column::auto().resizable(false))
.min_scrolled_height(0.0)
.sense(egui::Sense::click());
table
.header(20., |mut header| {
header.col(|ui| {
ui.strong("#");
});
header.col(|ui| {
ui.strong("Name");
});
header.col(|ui| {
ui.strong("Components");
});
header.col(|ui| {
ui.strong("Runtimes");
});
header.col(|_| {});
})
.body(|mut body| {
for i in 0..self.current_payloads.len() {
// let runtime = self.current_runtimes
body.row(18., |mut row| {
row.col(|ui| {
ui.label(i.to_string());
});
row.col(|ui| {
ui.label(self.current_payloads[i].name.clone());
});
row.col(|ui| {
ui.label(format!(
"{:?}",
self.current_payloads[i].components.clone()
));
});
row.col(|ui| {
ui.label("A");
});
row.col(|ui| {
if ui.button("Edit").clicked() {
self.state = ConfigState::EditConfig(
i,
self.current_payloads[i].clone(),
);
}
// if ui.button("Delete").clicked() {
// self.state = ConfigState::EditConfig(
// i,
// self.current_payloads[i].clone(),
// );
// }
});
if row.response().clicked() {
self.state = ConfigState::EditConfig(
i,
self.current_payloads[i].clone(),
);
}
});
}
});
});
});
});
}
fn edit_ui(ui: &mut Ui, config: &mut PayloadConfig) -> Option<bool> {
ui.horizontal(|ui| {
ui.label("Name");
ui.text_edit_singleline(&mut config.name);
});
ui.horizontal(|ui| {
ui.label("Components: ");
for component in vec!["client", "server"] {
let enabled = config
.components
.iter()
.enumerate()
.find(|s| s.1.eq(component));
if ui.selectable_label(enabled.is_some(), component).clicked() {
if let Some((i, _)) = enabled {
let _ = config.components.remove(i);
} else {
config.components.push(component.to_string());
}
}
}
});
let mut ret = None;
ui.horizontal(|ui| {
ret = if ui.button("Back").clicked() {
Some(false)
} else if ui.button("Save").clicked() {
Some(true)
} else {
None
};
});
ret
// return None;
}
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
struct PayloadConfig {
name: String,
components: Vec<String>,
// runtimes: Vec<RuntimeConfig>,
}
impl PayloadConfig {
pub fn new() -> Self {
Self {
name: "New Payload".to_string(),
components: Vec::new(),
// runtimes: Vec::new(),
pub fn update(&mut self, auth: &mut Auth, ui: &mut egui::Ui) {
if ui.button("Test").clicked() {
auth.test();
}
}
}
+230
View File
@@ -0,0 +1,230 @@
use egui::{TextStyle, Ui};
use egui_extras::{Column, TableBuilder};
#[derive(serde::Deserialize, serde::Serialize)]
pub struct Config {
state: ConfigState,
current_payloads: Vec<PayloadConfig>,
}
#[derive(serde::Deserialize, serde::Serialize)]
enum ConfigState {
Base,
NewConfig(PayloadConfig),
EditConfig(usize, PayloadConfig),
}
impl Default for Config {
fn default() -> Self {
Self {
state: ConfigState::Base,
current_payloads: vec![PayloadConfig {
name: "Test".to_string(),
components: vec!["server".to_string()],
// runtimes: Vec::new(),
}],
}
}
}
impl Config {
// pub fn title(&self) -> &str {
// match self.state {
// ConfigState::Base => "Config",
// ConfigState::NewConfig(..) => "Config/New",
// ConfigState::EditConfig(..) => "Config/Edit",
// }
// }
pub fn update(&mut self, ui: &mut Ui) {
match &mut self.state {
ConfigState::Base => self.table_ui(ui),
ConfigState::EditConfig(_, config) => {
ui.heading("Edit Payload");
match Self::edit_ui(ui, config) {
Some(true) => {
if let ConfigState::EditConfig(n, config) =
std::mem::replace(&mut self.state, ConfigState::Base)
{
self.current_payloads[n] = config;
}
}
Some(false) => {
self.state = ConfigState::Base;
}
_ => {}
}
}
ConfigState::NewConfig(config) => {
ui.heading("Edit Payload");
match Self::edit_ui(ui, config) {
Some(true) => {
if let ConfigState::NewConfig(config) =
std::mem::replace(&mut self.state, ConfigState::Base)
{
self.current_payloads.push(config);
}
}
Some(false) => {
self.state = ConfigState::Base;
}
_ => {}
}
}
}
}
fn table_ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
ui.heading("Payloads");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("New").clicked() {
self.state = ConfigState::NewConfig(PayloadConfig::new());
}
});
});
let body_text_size = TextStyle::Body.resolve(ui.style()).size;
use egui_extras::{Size, StripBuilder};
StripBuilder::new(ui)
.size(Size::remainder().at_least(100.0)) // for the table
.size(Size::exact(body_text_size)) // for the source code link
.vertical(|mut strip| {
strip.cell(|ui| {
egui::ScrollArea::horizontal().show(ui, |ui| {
let table = TableBuilder::new(ui)
.striped(true)
.resizable(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Column::auto().resizable(false))
.column(Column::remainder())
.column(Column::remainder())
.column(Column::remainder())
.column(Column::auto().resizable(false))
.min_scrolled_height(0.0)
.sense(egui::Sense::click());
table
.header(20., |mut header| {
header.col(|ui| {
ui.strong("#");
});
header.col(|ui| {
ui.strong("Name");
});
header.col(|ui| {
ui.strong("Components");
});
header.col(|ui| {
ui.strong("Runtimes");
});
header.col(|_| {});
})
.body(|mut body| {
for i in 0..self.current_payloads.len() {
// let runtime = self.current_runtimes
body.row(18., |mut row| {
row.col(|ui| {
ui.label(i.to_string());
});
row.col(|ui| {
ui.label(self.current_payloads[i].name.clone());
});
row.col(|ui| {
ui.label(format!(
"{:?}",
self.current_payloads[i].components.clone()
));
});
row.col(|ui| {
ui.label("A");
});
row.col(|ui| {
if ui.button("Edit").clicked() {
self.state = ConfigState::EditConfig(
i,
self.current_payloads[i].clone(),
);
}
// if ui.button("Delete").clicked() {
// self.state = ConfigState::EditConfig(
// i,
// self.current_payloads[i].clone(),
// );
// }
});
if row.response().clicked() {
self.state = ConfigState::EditConfig(
i,
self.current_payloads[i].clone(),
);
}
});
}
});
});
});
});
}
fn edit_ui(ui: &mut Ui, config: &mut PayloadConfig) -> Option<bool> {
ui.horizontal(|ui| {
ui.label("Name");
ui.text_edit_singleline(&mut config.name);
});
ui.horizontal(|ui| {
ui.label("Components: ");
for component in vec!["client", "server"] {
let enabled = config
.components
.iter()
.enumerate()
.find(|s| s.1.eq(component));
if ui.selectable_label(enabled.is_some(), component).clicked() {
if let Some((i, _)) = enabled {
let _ = config.components.remove(i);
} else {
config.components.push(component.to_string());
}
}
}
});
let mut ret = None;
ui.horizontal(|ui| {
ret = if ui.button("Back").clicked() {
Some(false)
} else if ui.button("Save").clicked() {
Some(true)
} else {
None
};
});
ret
// return None;
}
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
struct PayloadConfig {
name: String,
components: Vec<String>,
// runtimes: Vec<RuntimeConfig>,
}
impl PayloadConfig {
pub fn new() -> Self {
Self {
name: "New Payload".to_string(),
components: Vec::new(),
// runtimes: Vec::new(),
}
}
}
+6 -6
View File
@@ -93,6 +93,12 @@ impl FlowChart {
}
pub fn paint(&mut self, ui: &mut Ui) {
if ui.button("Arrange").clicked() {
for _ in 0..1_000 {
self.force(0.1);
}
}
let scene = Scene::new()
// .max_inner_size([350.0, 1000.0])
.zoom_range(0.1..=2.0);
@@ -135,11 +141,5 @@ impl FlowChart {
if response.double_clicked() {
self.scene_rect = inner_rect;
}
if ui.button("Arrange").clicked() {
for _ in 0..1_000 {
self.force(0.1);
}
}
}
}
+4 -2
View File
@@ -21,8 +21,10 @@ extern "C" {
#[wasm_bindgen(module = "/assets/sw.js")]
extern "C" {
pub fn httpGet(url: &str, callback: JsValue);
pub fn httpPost(url: &str, data: &str, callback: JsValue);
pub fn httpGet(url: &str, ok_callback: JsValue);
pub fn httpPost(url: &str, data: &str, ok_callback: JsValue);
pub fn httpGetAuth(url: &str, auth: String, ok_callback: JsValue);
pub fn httpPostAuth(url: &str, auth: String, data: &str, ok_callback: JsValue);
}
// }