Sling GUI

This commit is contained in:
2026-05-22 20:16:48 +02:00
parent 260986dfd0
commit 8d1165ca66
5 changed files with 6089 additions and 184 deletions
+5 -2
View File
@@ -10,6 +10,9 @@ bench = false
[dependencies]
libsvld = { path = "../libsvld" }
native-windows-gui = { version = "1.0.13", features = ["file-dialog"] }
native-windows-derive = "1.0.5"
can-dbc = "9.1.0"
slint = "1.16.1"
rfd = "0.17.2"
[build-dependencies]
slint-build = "1.16.1"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
slint_build::compile("ui/main.slint").unwrap();
}
+127 -122
View File
@@ -1,131 +1,136 @@
#![windows_subsystem = "windows"]
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
extern crate native_windows_gui as nwg;
extern crate native_windows_derive as nwd;
use std::{cell::RefCell, ffi::OsStr, fs, path::PathBuf};
use std::{
cell::RefCell,
fs,
path::PathBuf,
rc::Rc,
thread,
};
use can_dbc::Dbc;
use nwd::NwgUi;
use nwg::NativeUi;
use slint::ComponentHandle;
#[derive(Default, NwgUi)]
pub struct BasicApp {
#[nwg_control(size: (800, 200), title: "Sane Vector Log Decoder", flags: "WINDOW|VISIBLE")]
#[nwg_events( OnWindowClose: [BasicApp::say_goodbye] )]
window: nwg::Window,
slint::include_modules!();
#[nwg_layout(parent: window, spacing: 1)]
grid: nwg::GridLayout,
#[nwg_resource(title: "Select DBC", action: nwg::FileDialogAction::Open, filters: "DBC(*.dbc)|Any(*.*)")]
dbc_dialog: nwg::FileDialog,
#[nwg_resource(title: "Select ASC(s)", action: nwg::FileDialogAction::Open, multiselect: true, filters: "ASC(*.asc)|Any(*.*)")]
asc_dialog: nwg::FileDialog,
#[nwg_resource(title: "Select output directory", action: nwg::FileDialogAction::OpenDirectory)]
out_dialog: nwg::FileDialog,
#[nwg_control(readonly: true)]
#[nwg_layout_item(layout: grid, col: 1, row: 0, col_span: 2)]
dbc_file_name: nwg::TextInput,
#[nwg_control(readonly: true)]
#[nwg_layout_item(layout: grid, col: 1, row: 1, col_span: 2)]
asc_file_names: nwg::TextInput,
#[nwg_control(readonly: true)]
#[nwg_layout_item(layout: grid, col: 1, row: 2, col_span: 2)]
out_directory: nwg::TextInput,
#[nwg_control(text: "Select DBC")]
#[nwg_layout_item(layout: grid, col: 0, row: 0)]
#[nwg_events( OnButtonClick: [BasicApp::pick_dbc] )]
dbc_button: nwg::Button,
#[nwg_control(text: "Select ASC(s)")]
#[nwg_layout_item(layout: grid, col: 0, row: 1)]
#[nwg_events( OnButtonClick: [BasicApp::pick_asc] )]
asc_button: nwg::Button,
#[nwg_control(text: "Select output dir")]
#[nwg_layout_item(layout: grid, col: 0, row: 2)]
#[nwg_events( OnButtonClick: [BasicApp::pick_out] )]
outdir_button: nwg::Button,
#[nwg_control(text: "Zrób fikołka")]
#[nwg_layout_item(layout: grid, col: 0, row: 3, col_span: 3)]
#[nwg_events( OnButtonClick: [BasicApp::action] )]
action_button: nwg::Button,
dbc_path: RefCell<Option<PathBuf>>,
asc_paths: RefCell<Option<Vec<PathBuf>>>,
out_path: RefCell<Option<PathBuf>>,
}
impl BasicApp {
fn action(&self) {
let dbc_path = self.dbc_path.borrow();
let asc_paths = self.asc_paths.borrow();
let out_path = self.out_path.borrow();
if let Some(dbc_path) = dbc_path.as_ref() &&
let Some(asc_paths) = asc_paths.as_ref() &&
let Some(out_path) = out_path.as_ref() {
let dbc_data = fs::read_to_string(dbc_path).unwrap();
let dbc = Dbc::try_from(dbc_data.as_str()).unwrap();
for asc_path in asc_paths {
libsvld::decode(&dbc, asc_path, out_path.as_path());
}
nwg::modal_info_message(&self.window,
"Okej",
"Finished!");
} else {
nwg::modal_error_message(&self.window,
"ERROR",
"Select all the paths first, you idiot");
}
}
fn pick_dbc(&self) {
if self.dbc_dialog.run(Some(&self.window)) &&
let Ok(file) = self.dbc_dialog.get_selected_item() {
*self.dbc_path.borrow_mut() = Some(file.clone().try_into().unwrap());
self.dbc_file_name.set_text(&file.into_string().unwrap());
}
}
fn pick_asc(&self) {
println!("here");
if self.asc_dialog.run(Some(&self.window)) &&
let Ok(files) = self.asc_dialog.get_selected_items() {
*self.asc_paths.borrow_mut() = Some(files
.clone()
.iter()
.map(|x| x.try_into().unwrap())
.collect());
self.asc_file_names.set_text(&files.join(OsStr::new("; ")).into_string().unwrap());
}
}
fn pick_out(&self) {
if self.out_dialog.run(Some(&self.window)) &&
let Ok(path) = self.out_dialog.get_selected_item() {
*self.out_path.borrow_mut() = Some(path.clone().try_into().unwrap());
self.out_directory.set_text(&path.into_string().unwrap());
}
}
fn say_goodbye(&self) {
nwg::stop_thread_dispatch();
}
#[derive(Default)]
struct State {
dbc_path: Option<PathBuf>,
asc_paths: Vec<PathBuf>,
output_dir: Option<PathBuf>,
}
fn main() {
nwg::init().expect("Failed to init Native Windows GUI");
nwg::Font::set_global_family("Segoe UI").expect("Failed to set default font");
let _app = BasicApp::build_ui(Default::default()).expect("Failed to build UI");
nwg::dispatch_thread_events();
}
let window = MainWindow::new().unwrap();
let state = Rc::new(RefCell::new(State::default()));
window.on_browse_dbc({
let window_weak = window.as_weak();
let state = state.clone();
move || {
if let Some(path) = rfd::FileDialog::new()
.set_title("Select DBC")
.add_filter("DBC files", &["dbc"])
.pick_file()
{
let display = path.display().to_string();
state.borrow_mut().dbc_path = Some(path);
if let Some(w) = window_weak.upgrade() {
w.set_dbc_path(display.into());
}
}
}
});
window.on_browse_asc({
let window_weak = window.as_weak();
let state = state.clone();
move || {
if let Some(paths) = rfd::FileDialog::new()
.set_title("Select ASC file(s)")
.add_filter("ASC files", &["asc"])
.pick_files()
{
let label = match paths.len() {
0 => String::new(),
1 => paths[0].display().to_string(),
n => format!("{} file(s) selected", n),
};
let has_asc = !paths.is_empty();
state.borrow_mut().asc_paths = paths;
if let Some(w) = window_weak.upgrade() {
w.set_asc_label(label.into());
w.set_has_asc(has_asc);
}
}
}
});
window.on_browse_output({
let window_weak = window.as_weak();
let state = state.clone();
move || {
if let Some(path) = rfd::FileDialog::new()
.set_title("Select output directory")
.pick_folder()
{
let display = path.display().to_string();
state.borrow_mut().output_dir = Some(path);
if let Some(w) = window_weak.upgrade() {
w.set_output_dir(display.into());
}
}
}
});
window.on_decode({
let window_weak = window.as_weak();
let state = state.clone();
move || {
let s = state.borrow();
let (Some(dbc_path), Some(output_dir)) = (s.dbc_path.clone(), s.output_dir.clone())
else {
return;
};
let asc_paths = s.asc_paths.clone();
if asc_paths.is_empty() {
return;
}
drop(s);
if let Some(w) = window_weak.upgrade() {
w.set_busy(true);
w.set_status("Decoding...".into());
}
let window_weak = window_weak.clone();
thread::spawn(move || {
let result = (|| -> Result<(), String> {
let data = fs::read_to_string(&dbc_path)
.map_err(|e| format!("Failed to read DBC: {}", e))?;
let dbc = Dbc::try_from(data.as_str())
.map_err(|e| format!("Failed to parse DBC: {:?}", e))?;
for asc in &asc_paths {
libsvld::decode(&dbc, asc, &output_dir);
}
Ok(())
})();
let message = match result {
Ok(()) => "Done.".to_string(),
Err(e) => format!("Error: {}", e),
};
slint::invoke_from_event_loop(move || {
if let Some(w) = window_weak.upgrade() {
w.set_busy(false);
w.set_status(message.into());
}
})
.ok();
});
}
});
window.run().unwrap();
}
+108
View File
@@ -0,0 +1,108 @@
import { Button, LineEdit, CheckBox, Spinner, GridBox, VerticalBox, HorizontalBox } from "std-widgets.slint";
export component MainWindow inherits Window {
title: "Sane Vector Log Decoder";
preferred-width: 640px;
preferred-height: 320px;
in-out property <string> dbc-path;
in-out property <string> asc-label;
in-out property <bool> has-asc;
in-out property <string> output-dir;
in-out property <bool> laps;
in-out property <bool> busy;
in-out property <string> status;
callback browse-dbc();
callback browse-asc();
callback browse-output();
callback decode();
VerticalBox {
padding: 12px;
spacing: 8px;
GridBox {
spacing: 8px;
Row {
Text {
text: "DBC file:";
vertical-alignment: center;
}
LineEdit {
read-only: true;
text: root.dbc-path;
}
Button {
text: "Browse...";
clicked => { root.browse-dbc(); }
}
}
Row {
Text {
text: "ASC file(s):";
vertical-alignment: center;
}
LineEdit {
read-only: true;
text: root.asc-label;
}
Button {
text: "Browse...";
clicked => { root.browse-asc(); }
}
}
Row {
Text {
text: "Output dir:";
vertical-alignment: center;
}
LineEdit {
read-only: true;
text: root.output-dir;
}
Button {
text: "Browse...";
clicked => { root.browse-output(); }
}
}
}
CheckBox {
text: "Generate lap beacons";
checked <=> root.laps;
}
Rectangle { vertical-stretch: 1; }
HorizontalBox {
padding: 0px;
spacing: 8px;
if root.busy: Spinner {
width: 20px;
height: 20px;
indeterminate: true;
}
Text {
text: root.status;
horizontal-stretch: 1;
vertical-alignment: center;
overflow: elide;
}
Button {
text: "Decode!";
enabled: !root.busy
&& root.dbc-path != ""
&& root.has-asc
&& root.output-dir != "";
clicked => { root.decode(); }
}
}
}
}