mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-04-20 11:35:48 +00:00
feat!: migrated ui toolkit to relm4/gtk
This commit is contained in:
parent
0106168146
commit
24b28bd760
13 changed files with 1419 additions and 2471 deletions
2751
Cargo.lock
generated
2751
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
@ -7,14 +7,16 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
expect-dialog = "1.0.0"
|
||||
iced = { version = "0.9.0", features = [
|
||||
"smol"
|
||||
] }
|
||||
iced_aw = { version = "0.5.2", default-features = false, features = [
|
||||
"tab_bar", "tabs", "quad", "modal", "card"
|
||||
] }
|
||||
nix = "0.26.2"
|
||||
relm4 = { version = "0.6.0", features = [
|
||||
"libadwaita"
|
||||
] }
|
||||
relm4-components = "0.6.0"
|
||||
relm4-icons = { version = "0.6.0", features = [
|
||||
"menu", "loupe"
|
||||
] }
|
||||
serde = { version = "1.0.163", features = [
|
||||
"derive"
|
||||
] }
|
||||
serde_json = "1.0.96"
|
||||
tracker = "0.2.1"
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::{
|
|||
constants::CMD_NAME,
|
||||
file_utils::{get_config_dir, get_writer},
|
||||
};
|
||||
use expect_dialog::ExpectDialog;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fs::File, io::BufReader};
|
||||
|
||||
|
@ -49,8 +50,8 @@ fn save_config_to_path(config: &Config, path_s: &String) -> Result<(), serde_jso
|
|||
serde_json::to_writer_pretty(writer, config)
|
||||
}
|
||||
|
||||
pub fn save_config(config: &Config) -> Result<(), serde_json::Error> {
|
||||
save_config_to_path(config, &config_file_path())
|
||||
pub fn save_config(config: &Config) {
|
||||
save_config_to_path(config, &config_file_path()).expect_dialog("Failed to save config");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
pub const APP_NAME: &str = "Rex2";
|
||||
pub const APP_ID: &str = "org.gabmus.rex2";
|
||||
pub const CMD_NAME: &str = "rex2";
|
||||
pub const VERSION: &str = "0.1";
|
||||
pub const REPO_URL: &str = "https://gitlab.com/gabmus/rex2";
|
||||
pub const SINGLE_DEVELOPER: &str = "The Rex2 Team";
|
||||
|
||||
pub fn get_developers() -> Vec<String> {
|
||||
vec!["Gabriele Musco <gabmus@disroot.org>".to_string()]
|
||||
}
|
||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -1,6 +1,6 @@
|
|||
use expect_dialog::ExpectDialog;
|
||||
use iced::{Application, Settings};
|
||||
use ui::main_win::MainWin;
|
||||
use constants::APP_ID;
|
||||
use relm4::RelmApp;
|
||||
use ui::app::App;
|
||||
|
||||
pub mod builders;
|
||||
pub mod config;
|
||||
|
@ -15,5 +15,7 @@ pub mod runner;
|
|||
pub mod ui;
|
||||
|
||||
fn main() {
|
||||
MainWin::run(Settings::default()).expect_dialog("Application encountered an error");
|
||||
let app = RelmApp::new(APP_ID);
|
||||
relm4_icons::initialize_icons();
|
||||
app.run::<App>(())
|
||||
}
|
||||
|
|
|
@ -148,16 +148,25 @@ impl Runner {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_output(&mut self) -> String {
|
||||
fn receive_output(&mut self) {
|
||||
loop {
|
||||
match self.receiver.try_recv() {
|
||||
Ok(data) => self.output.push(data),
|
||||
Err(_) => break,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_output(&mut self) -> String {
|
||||
self.receive_output();
|
||||
self.output.concat()
|
||||
}
|
||||
|
||||
pub fn get_rows(&mut self) -> Vec<String> {
|
||||
self.receive_output();
|
||||
self.output.clone()
|
||||
}
|
||||
|
||||
fn save_log(path_s: String, log: &Vec<String>) -> Result<(), std::io::Error> {
|
||||
let mut writer = get_writer(&path_s);
|
||||
let log_s = log.concat();
|
||||
|
|
42
src/ui/about_dialog.rs
Normal file
42
src/ui/about_dialog.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use relm4::gtk::traits::GtkWindowExt;
|
||||
use relm4::prelude::*;
|
||||
use relm4::{ComponentParts, SimpleComponent};
|
||||
|
||||
use crate::constants::{APP_NAME, REPO_URL, SINGLE_DEVELOPER, VERSION, get_developers};
|
||||
|
||||
pub struct AboutDialog {}
|
||||
|
||||
impl SimpleComponent for AboutDialog {
|
||||
type Init = ();
|
||||
type Input = ();
|
||||
type Output = ();
|
||||
type Root = adw::AboutWindow;
|
||||
type Widgets = adw::AboutWindow;
|
||||
|
||||
fn init_root() -> Self::Root {
|
||||
adw::AboutWindow::builder()
|
||||
.application_name(APP_NAME)
|
||||
.license_type(gtk::License::Agpl30)
|
||||
.version(VERSION)
|
||||
.website(REPO_URL)
|
||||
.developer_name(SINGLE_DEVELOPER)
|
||||
.developers(get_developers())
|
||||
.modal(true)
|
||||
.hide_on_close(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: &Self::Root,
|
||||
sender: relm4::ComponentSender<Self>,
|
||||
) -> relm4::ComponentParts<Self> {
|
||||
let model = AboutDialog {};
|
||||
let widgets = root.clone();
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update_view(&self, dialog: &mut Self::Widgets, _sender: ComponentSender<Self>) {
|
||||
dialog.present();
|
||||
}
|
||||
}
|
299
src/ui/app.rs
Normal file
299
src/ui/app.rs
Normal file
|
@ -0,0 +1,299 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::builders::build_monado::get_build_monado_runner;
|
||||
use crate::config::{get_config, save_config, Config};
|
||||
use crate::constants::APP_NAME;
|
||||
use crate::profile::Profile;
|
||||
use crate::profiles::valve_index::valve_index_profile;
|
||||
use crate::runner::{Runner, RunnerStatus};
|
||||
use crate::ui::build_window::BuildWindowMsg;
|
||||
use crate::ui::debug_view::DebugViewInit;
|
||||
use crate::ui::main_view::{MainView, MainViewInit, MainViewOutMsg};
|
||||
use expect_dialog::ExpectDialog;
|
||||
use gtk::prelude::*;
|
||||
use relm4::actions::{ActionGroupName, RelmAction, RelmActionGroup};
|
||||
use relm4::gtk::glib;
|
||||
use relm4::{new_action_group, new_stateful_action, new_stateless_action, prelude::*};
|
||||
use relm4::{ComponentParts, ComponentSender, SimpleComponent};
|
||||
|
||||
use super::about_dialog::AboutDialog;
|
||||
use super::build_window::BuildWindow;
|
||||
use super::debug_view::{DebugView, DebugViewMsg};
|
||||
use super::main_view::MainViewMsg;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct App {
|
||||
enable_debug_view: bool,
|
||||
|
||||
#[tracker::do_not_track]
|
||||
main_view: Controller<MainView>,
|
||||
#[tracker::do_not_track]
|
||||
debug_view: Controller<DebugView>,
|
||||
#[tracker::do_not_track]
|
||||
about_dialog: Controller<AboutDialog>,
|
||||
#[tracker::do_not_track]
|
||||
build_window: Controller<BuildWindow>,
|
||||
|
||||
#[tracker::do_not_track]
|
||||
config: Config,
|
||||
#[tracker::do_not_track]
|
||||
monado_runner: Option<Runner>,
|
||||
#[tracker::do_not_track]
|
||||
build_runner: Option<Runner>,
|
||||
#[tracker::do_not_track]
|
||||
profiles: Vec<Profile>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
UpdateView,
|
||||
BuildProfile,
|
||||
EnableDebugViewChanged(bool),
|
||||
DoStartStopMonado,
|
||||
ProfileSelected(String),
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn get_profile_by_name(&self, name: &String) -> Option<&Profile> {
|
||||
for profile in &self.profiles {
|
||||
if &profile.name == name {
|
||||
return Some(profile);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn get_selected_profile(&self) -> Option<&Profile> {
|
||||
self.get_profile_by_name(&self.config.selected_profile_name)
|
||||
}
|
||||
|
||||
pub fn start_monado(&mut self) {
|
||||
let mut runner = Runner::monado_runner_from_profile(
|
||||
self.get_selected_profile()
|
||||
.expect_dialog("Could not find selected profile")
|
||||
.clone(),
|
||||
);
|
||||
runner.start();
|
||||
self.monado_runner = Some(runner);
|
||||
}
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for App {
|
||||
type Init = ();
|
||||
type Input = Msg;
|
||||
type Output = ();
|
||||
|
||||
view! {
|
||||
#[root]
|
||||
adw::ApplicationWindow {
|
||||
set_title: Some(APP_NAME),
|
||||
set_default_size: (400, 300),
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
append: model.main_view.widget(),
|
||||
append: sep = >k::Separator {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
#[track = "model.changed(App::enable_debug_view())"]
|
||||
set_visible: model.enable_debug_view,
|
||||
},
|
||||
append: model.debug_view.widget(),
|
||||
// append: model.debug_view.widget(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shutdown(&mut self, widgets: &mut Self::Widgets, output: relm4::Sender<Self::Output>) {
|
||||
match &mut self.monado_runner {
|
||||
None => {}
|
||||
Some(runner) => {
|
||||
runner.terminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
Msg::UpdateView => {
|
||||
match &mut self.monado_runner {
|
||||
None => {}
|
||||
Some(runner) => match runner.status() {
|
||||
RunnerStatus::Running => {
|
||||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::LogUpdated(runner.get_rows()));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
};
|
||||
match &mut self.build_runner {
|
||||
None => {}
|
||||
Some(runner) => match runner.status() {
|
||||
RunnerStatus::Running => {
|
||||
self.build_window.sender().emit(BuildWindowMsg::UpdateContent(runner.get_output()));
|
||||
}
|
||||
RunnerStatus::Stopped => {
|
||||
self.build_window.sender().emit(BuildWindowMsg::UpdateCanClose(true));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Msg::EnableDebugViewChanged(val) => {
|
||||
self.set_enable_debug_view(val);
|
||||
self.config.debug_view_enabled = val;
|
||||
save_config(&self.config);
|
||||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::EnableDebugViewChanged(val));
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::EnableDebugViewChanged(val));
|
||||
}
|
||||
Msg::DoStartStopMonado => match &mut self.monado_runner {
|
||||
None => {
|
||||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::LogUpdated(vec![]));
|
||||
self.start_monado();
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::MonadoActiveChanged(true));
|
||||
}
|
||||
Some(runner) => match runner.status() {
|
||||
RunnerStatus::Running => {
|
||||
runner.terminate();
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::MonadoActiveChanged(false));
|
||||
}
|
||||
RunnerStatus::Stopped => {
|
||||
self.debug_view
|
||||
.sender()
|
||||
.emit(DebugViewMsg::LogUpdated(vec![]));
|
||||
self.start_monado();
|
||||
self.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::MonadoActiveChanged(true));
|
||||
}
|
||||
},
|
||||
},
|
||||
Msg::BuildProfile => {
|
||||
let profile = self
|
||||
.get_selected_profile()
|
||||
.expect_dialog("Could not find selected profile");
|
||||
let mut runner = get_build_monado_runner(profile.clone());
|
||||
runner.start();
|
||||
self.build_window.sender().emit(BuildWindowMsg::UpdateTitle("Building Monado".into()));
|
||||
self.build_window.sender().emit(BuildWindowMsg::UpdateCanClose(false));
|
||||
self.build_runner = Some(runner);
|
||||
},
|
||||
Msg::ProfileSelected(prof_name) => {
|
||||
self.config.selected_profile_name = prof_name;
|
||||
save_config(&self.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
_params: Self::Init,
|
||||
root: &Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let config = get_config();
|
||||
let profiles = vec![valve_index_profile()];
|
||||
let model = App {
|
||||
main_view: MainView::builder()
|
||||
.launch(MainViewInit {
|
||||
config: config.clone(),
|
||||
})
|
||||
.forward(sender.input_sender(), |message| match message {
|
||||
MainViewOutMsg::UpdateView => Msg::UpdateView,
|
||||
MainViewOutMsg::EnableDebugViewChanged(val) => Msg::EnableDebugViewChanged(val),
|
||||
MainViewOutMsg::DoStartStopMonado => Msg::DoStartStopMonado,
|
||||
MainViewOutMsg::ProfileSelected(name) => Msg::ProfileSelected(name),
|
||||
}),
|
||||
debug_view: DebugView::builder()
|
||||
.launch(DebugViewInit {
|
||||
enable_debug_view: config.debug_view_enabled,
|
||||
})
|
||||
.forward(sender.input_sender(), |message| match message {
|
||||
_ => Msg::UpdateView,
|
||||
}),
|
||||
about_dialog: AboutDialog::builder()
|
||||
.transient_for(root)
|
||||
.launch(())
|
||||
.detach(),
|
||||
build_window: BuildWindow::builder()
|
||||
.transient_for(root)
|
||||
.launch(())
|
||||
.detach(),
|
||||
enable_debug_view: config.debug_view_enabled,
|
||||
config,
|
||||
tracker: 0,
|
||||
profiles,
|
||||
monado_runner: None,
|
||||
build_runner: None,
|
||||
};
|
||||
let widgets = view_output!();
|
||||
|
||||
let mut actions = RelmActionGroup::<AppActionGroup>::new();
|
||||
|
||||
let buildprofile_action = {
|
||||
let buildprofile_sender = model.build_window.sender().clone();
|
||||
let this_sender = sender.clone();
|
||||
RelmAction::<BuildProfileAction>::new_stateless(move |_| {
|
||||
buildprofile_sender.send(BuildWindowMsg::Present).unwrap();
|
||||
this_sender.input_sender().emit(Msg::BuildProfile);
|
||||
})
|
||||
};
|
||||
|
||||
let about_action = {
|
||||
let abd_sender = model.about_dialog.sender().clone();
|
||||
RelmAction::<AboutAction>::new_stateless(move |_| {
|
||||
abd_sender.send(()).unwrap();
|
||||
})
|
||||
};
|
||||
|
||||
let debug_view_toggle_action: RelmAction<DebugViewToggleAction> = {
|
||||
let debugtoggle_sender = sender.clone();
|
||||
RelmAction::<DebugViewToggleAction>::new_stateful(
|
||||
&model.enable_debug_view,
|
||||
move |_, state| {
|
||||
let s = *state;
|
||||
*state = !s;
|
||||
debugtoggle_sender.input(Msg::EnableDebugViewChanged(*state));
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
actions.add_action(about_action);
|
||||
actions.add_action(buildprofile_action);
|
||||
actions.add_action(debug_view_toggle_action);
|
||||
|
||||
root.insert_action_group(AppActionGroup::NAME, Some(&actions.into_action_group()));
|
||||
|
||||
model.main_view.sender().emit(MainViewMsg::UpdateProfileNames(model.profiles.iter().map(|p| p.clone().name).collect()));
|
||||
|
||||
let timer_sender = sender.clone();
|
||||
glib::timeout_add_local(Duration::from_millis(1000), move || {
|
||||
timer_sender.input(Msg::UpdateView);
|
||||
return glib::Continue(true);
|
||||
});
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
}
|
||||
|
||||
new_action_group!(pub AppActionGroup, "win");
|
||||
new_stateless_action!(pub AboutAction, AppActionGroup, "about");
|
||||
new_stateless_action!(pub BuildProfileAction, AppActionGroup, "buildprofile");
|
||||
new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool);
|
130
src/ui/build_window.rs
Normal file
130
src/ui/build_window.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
use gtk::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct BuildWindow {
|
||||
title: String,
|
||||
content: String,
|
||||
can_close: bool,
|
||||
|
||||
#[tracker::do_not_track]
|
||||
pub textbuf: gtk::TextBuffer,
|
||||
#[tracker::do_not_track]
|
||||
pub win: Option<adw::Window>,
|
||||
#[tracker::do_not_track]
|
||||
pub scrolled_win: Option<gtk::ScrolledWindow>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BuildWindowMsg {
|
||||
Present,
|
||||
UpdateTitle(String),
|
||||
UpdateContent(String),
|
||||
UpdateCanClose(bool),
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for BuildWindow {
|
||||
type Init = ();
|
||||
type Input = BuildWindowMsg;
|
||||
type Output = ();
|
||||
|
||||
view! {
|
||||
#[name(win)]
|
||||
adw::Window {
|
||||
set_modal: true,
|
||||
set_default_size: (520, 400),
|
||||
gtk::Box {
|
||||
set_vexpand: true,
|
||||
set_hexpand: true,
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
gtk::WindowHandle {
|
||||
set_vexpand: false,
|
||||
set_hexpand: true,
|
||||
adw::HeaderBar {
|
||||
set_show_end_title_buttons: false,
|
||||
set_show_start_title_buttons: false,
|
||||
add_css_class: "flat",
|
||||
#[wrap(Some)]
|
||||
set_title_widget: title_label = >k::Label {
|
||||
#[track = "model.changed(BuildWindow::title())"]
|
||||
set_label: &model.title,
|
||||
add_css_class: "title",
|
||||
},
|
||||
}
|
||||
},
|
||||
#[name(scrolled_win)]
|
||||
gtk::ScrolledWindow {
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
set_margin_all: 12,
|
||||
add_css_class: "card",
|
||||
set_overflow: gtk::Overflow::Hidden,
|
||||
gtk::TextView {
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
set_editable: false,
|
||||
set_monospace: true,
|
||||
set_buffer: Some(&model.textbuf),
|
||||
},
|
||||
},
|
||||
gtk::Button {
|
||||
add_css_class: "pill",
|
||||
set_halign: gtk::Align::Center,
|
||||
set_label: "Close",
|
||||
set_margin_all: 12,
|
||||
#[track = "model.changed(BuildWindow::can_close())"]
|
||||
set_sensitive: model.can_close,
|
||||
connect_clicked[win] => move |_| {
|
||||
win.hide();
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
BuildWindowMsg::Present => {
|
||||
self.win.as_ref().unwrap().present();
|
||||
},
|
||||
BuildWindowMsg::UpdateTitle(t) => {
|
||||
self.set_title(t);
|
||||
},
|
||||
BuildWindowMsg::UpdateContent(c) => {
|
||||
self.set_content(c);
|
||||
self.textbuf.set_text(&self.content);
|
||||
let sw = self.scrolled_win.as_ref().unwrap().clone();
|
||||
let adj = sw.vadjustment();
|
||||
// upper means highest value = lowest point in the sw
|
||||
adj.set_value(adj.upper());
|
||||
sw.set_vadjustment(Some(&adj));
|
||||
},
|
||||
BuildWindowMsg::UpdateCanClose(val) => {
|
||||
self.set_can_close(val);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: &Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let mut model = BuildWindow {
|
||||
tracker: 0,
|
||||
title: "".into(),
|
||||
content: "".into(),
|
||||
can_close: false,
|
||||
textbuf: gtk::TextBuffer::builder().build(),
|
||||
win: None,
|
||||
scrolled_win: None,
|
||||
};
|
||||
let widgets = view_output!();
|
||||
model.scrolled_win = Some(widgets.scrolled_win.clone());
|
||||
model.win = Some(widgets.win.clone());
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
}
|
172
src/ui/debug_view.rs
Normal file
172
src/ui/debug_view.rs
Normal file
|
@ -0,0 +1,172 @@
|
|||
use gtk::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
use relm4::{ComponentSender, SimpleComponent};
|
||||
use relm4_icons::icon_name;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DebugLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl DebugLevel {
|
||||
pub fn from_string(s: String) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"debug" => Self::Debug,
|
||||
"info" => Self::Info,
|
||||
"warning" => Self::Warning,
|
||||
"error" => Self::Error,
|
||||
_ => Self::Debug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const DEBUG_LEVEL_STRINGS: [&str; 4] = ["Debug", "Info", "Warning", "Error"];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DebugViewMsg {
|
||||
DebugLevelChanged(DebugLevel),
|
||||
SearchTermChanged(String),
|
||||
LogUpdated(Vec<String>),
|
||||
EnableDebugViewChanged(bool),
|
||||
}
|
||||
|
||||
#[tracker::track]
|
||||
pub struct DebugView {
|
||||
#[tracker::do_not_track]
|
||||
pub debug_level: DebugLevel,
|
||||
#[tracker::do_not_track]
|
||||
pub search_term: String,
|
||||
#[tracker::do_not_track]
|
||||
pub log: String,
|
||||
#[tracker::do_not_track]
|
||||
pub textbuf: gtk::TextBuffer,
|
||||
#[tracker::do_not_track]
|
||||
pub scrolled_win: Option<gtk::ScrolledWindow>,
|
||||
#[tracker::do_not_track]
|
||||
pub searchbar: Option<gtk::SearchBar>,
|
||||
#[tracker::do_not_track]
|
||||
pub search_entry: Option<gtk::SearchEntry>,
|
||||
|
||||
pub enable_debug_view: bool,
|
||||
}
|
||||
|
||||
pub struct DebugViewInit {
|
||||
pub enable_debug_view: bool,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for DebugView {
|
||||
type Init = DebugViewInit;
|
||||
type Input = DebugViewMsg;
|
||||
type Output = ();
|
||||
|
||||
view! {
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
#[track = "model.changed(DebugView::enable_debug_view())"]
|
||||
set_visible: model.enable_debug_view,
|
||||
gtk::WindowHandle {
|
||||
set_hexpand: true,
|
||||
set_vexpand: false,
|
||||
adw::HeaderBar {
|
||||
#[wrap(Some)]
|
||||
set_title_widget: title_label = >k::Label {
|
||||
set_label: "Debug View",
|
||||
add_css_class: "title",
|
||||
},
|
||||
pack_end: search_toggle = >k::ToggleButton {
|
||||
set_icon_name: icon_name::LOUPE,
|
||||
set_tooltip_text: Some("Filter Log"),
|
||||
},
|
||||
pack_start: log_level_dropdown = >k::DropDown::from_strings(&DEBUG_LEVEL_STRINGS) {
|
||||
},
|
||||
},
|
||||
},
|
||||
#[name(searchbar)]
|
||||
gtk::SearchBar {
|
||||
set_hexpand: true,
|
||||
#[chain(flags(gtk::glib::BindingFlags::BIDIRECTIONAL).build())]
|
||||
bind_property: ("search-mode-enabled", &search_toggle, "active"),
|
||||
#[wrap(Some)]
|
||||
set_child: search_entry = >k::SearchEntry {
|
||||
set_hexpand: true,
|
||||
},
|
||||
connect_entry: &search_entry,
|
||||
},
|
||||
#[name(scrolled_win)]
|
||||
gtk::ScrolledWindow {
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
gtk::TextView {
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
set_editable: false,
|
||||
set_monospace: true,
|
||||
set_buffer: Some(&model.textbuf)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
DebugViewMsg::DebugLevelChanged(level) => {}
|
||||
DebugViewMsg::SearchTermChanged(term) => {}
|
||||
DebugViewMsg::LogUpdated(n_log) => {
|
||||
let searchbar = self.searchbar.as_ref().unwrap().clone();
|
||||
let search_entry = self.search_entry.as_ref().unwrap().clone();
|
||||
let mut search_text: String = search_entry.text().into();
|
||||
search_text = search_text.trim().to_lowercase();
|
||||
if searchbar.is_search_mode() && !search_text.is_empty() {
|
||||
self.log = n_log
|
||||
.iter()
|
||||
.filter(|row| row.to_lowercase().contains(&search_text))
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.concat();
|
||||
} else {
|
||||
self.log = n_log.concat();
|
||||
}
|
||||
self.textbuf.set_text(&self.log);
|
||||
let sw = self.scrolled_win.as_ref().unwrap().clone();
|
||||
let adj = sw.vadjustment();
|
||||
// upper means highest value = lowest point in the sw
|
||||
adj.set_value(adj.upper());
|
||||
sw.set_vadjustment(Some(&adj));
|
||||
}
|
||||
DebugViewMsg::EnableDebugViewChanged(val) => self.set_enable_debug_view(val),
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: &Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let mut model = DebugView {
|
||||
tracker: 0,
|
||||
debug_level: DebugLevel::Debug,
|
||||
search_term: "".into(),
|
||||
log: "".into(),
|
||||
textbuf: gtk::TextBuffer::builder().build(),
|
||||
enable_debug_view: init.enable_debug_view,
|
||||
scrolled_win: None,
|
||||
searchbar: None,
|
||||
search_entry: None,
|
||||
};
|
||||
|
||||
let widgets = view_output!();
|
||||
model.scrolled_win = Some(widgets.scrolled_win.clone());
|
||||
model.searchbar = Some(widgets.searchbar.clone());
|
||||
model.search_entry = Some(widgets.search_entry.clone());
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
}
|
141
src/ui/main_view.rs
Normal file
141
src/ui/main_view.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use gtk::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
use relm4::{SimpleComponent, ComponentSender, ComponentParts};
|
||||
use relm4_icons::icon_name;
|
||||
use crate::config::Config;
|
||||
use crate::constants::APP_NAME;
|
||||
use crate::ui::app::{AboutAction, DebugViewToggleAction, BuildProfileAction};
|
||||
|
||||
#[tracker::track]
|
||||
pub struct MainView {
|
||||
monado_active: bool,
|
||||
enable_debug_view: bool,
|
||||
profile_names: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MainViewMsg {
|
||||
StartStopClicked,
|
||||
MonadoActiveChanged(bool),
|
||||
EnableDebugViewChanged(bool),
|
||||
UpdateProfileNames(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MainViewOutMsg {
|
||||
UpdateView,
|
||||
EnableDebugViewChanged(bool),
|
||||
DoStartStopMonado,
|
||||
ProfileSelected(String),
|
||||
}
|
||||
|
||||
pub struct MainViewInit {
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for MainView {
|
||||
type Init = MainViewInit;
|
||||
type Input = MainViewMsg;
|
||||
type Output = MainViewOutMsg;
|
||||
|
||||
menu! {
|
||||
app_menu: {
|
||||
section! {
|
||||
// value inside action is ignored
|
||||
"_Debug View" => DebugViewToggleAction,
|
||||
"_Build Profile" => BuildProfileAction,
|
||||
},
|
||||
section! {
|
||||
"_About" => AboutAction,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
#[track = "model.changed(MainView::enable_debug_view())"]
|
||||
set_hexpand: !model.enable_debug_view,
|
||||
set_vexpand: true,
|
||||
set_size_request: (270, 350),
|
||||
gtk::WindowHandle {
|
||||
set_hexpand: true,
|
||||
set_vexpand: false,
|
||||
adw::HeaderBar {
|
||||
#[wrap(Some)]
|
||||
set_title_widget: title_label = >k::Label {
|
||||
set_label: APP_NAME,
|
||||
add_css_class: "title",
|
||||
},
|
||||
pack_end: menu_btn = >k::MenuButton {
|
||||
set_icon_name: icon_name::MENU,
|
||||
set_menu_model: Some(&app_menu),
|
||||
},
|
||||
pack_start: profiles_dropdown = >k::DropDown {
|
||||
set_tooltip_text: Some("Profiles"),
|
||||
#[track = "model.changed(MainView::profile_names())"]
|
||||
set_model: Some(&{
|
||||
let names: Vec<_> = model.profile_names.iter().map(String::as_str).collect();
|
||||
gtk::StringList::new(&names)
|
||||
}),
|
||||
},
|
||||
#[track = "model.changed(MainView::enable_debug_view())"]
|
||||
set_show_end_title_buttons: !model.enable_debug_view,
|
||||
},
|
||||
},
|
||||
gtk::Box {
|
||||
set_spacing: 12,
|
||||
set_margin_all: 12,
|
||||
gtk::Button {
|
||||
add_css_class: "pill",
|
||||
add_css_class: "suggested-action",
|
||||
add_css_class: "destructive-action",
|
||||
set_hexpand: true,
|
||||
set_halign: gtk::Align::Center,
|
||||
#[track = "model.changed(MainView::monado_active())"]
|
||||
set_class_active: ("suggested-action", !model.monado_active),
|
||||
#[track = "model.changed(MainView::monado_active())"]
|
||||
set_label: match model.monado_active {
|
||||
true => "Stop",
|
||||
false => "Start",
|
||||
},
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(MainViewMsg::StartStopClicked)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
MainViewMsg::StartStopClicked => {
|
||||
sender.output(MainViewOutMsg::DoStartStopMonado);
|
||||
},
|
||||
MainViewMsg::MonadoActiveChanged(active) => {
|
||||
self.set_monado_active(active);
|
||||
}
|
||||
MainViewMsg::EnableDebugViewChanged(val) => {
|
||||
self.set_enable_debug_view(val);
|
||||
}
|
||||
MainViewMsg::UpdateProfileNames(names) => {
|
||||
self.set_profile_names(names);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init(init: Self::Init, root: &Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
|
||||
let model = MainView {
|
||||
monado_active: false,
|
||||
enable_debug_view: init.config.debug_view_enabled,
|
||||
profile_names: vec![],
|
||||
tracker: 0,
|
||||
};
|
||||
let widgets = view_output!();
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
}
|
|
@ -1,300 +0,0 @@
|
|||
use super::styles::bordered_container::BorderedContainer;
|
||||
use crate::{
|
||||
builders::build_monado::get_build_monado_runner,
|
||||
config::{get_config, save_config, Config},
|
||||
constants::APP_NAME,
|
||||
profile::Profile,
|
||||
profiles::valve_index::valve_index_profile,
|
||||
runner::{Runner, RunnerStatus},
|
||||
ui::widgets::widgets::vseparator,
|
||||
};
|
||||
use expect_dialog::ExpectDialog;
|
||||
use iced::{
|
||||
executor,
|
||||
theme::{Button, Container},
|
||||
time,
|
||||
widget::{
|
||||
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space,
|
||||
},
|
||||
Alignment, Application, Command, Element, Length, Padding, Subscription, Theme,
|
||||
};
|
||||
use iced_aw::{Card, Modal};
|
||||
use std::{
|
||||
cell::Cell,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub struct MainWin {
|
||||
profiles: Vec<Profile>,
|
||||
config: Config,
|
||||
debug_search_term: String,
|
||||
monado_runner: Cell<Runner>,
|
||||
monado_active: bool,
|
||||
monado_log: String,
|
||||
build_monado_runner: Option<Runner>,
|
||||
show_build_monado_modal: bool,
|
||||
build_monado_log: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Start,
|
||||
Stop,
|
||||
Tick,
|
||||
ProfileChanged(Profile),
|
||||
EditProfile,
|
||||
DebugViewChanged(bool),
|
||||
LogLevelChanged(String),
|
||||
DebugSearchChanged(String),
|
||||
LogUpdate(Instant),
|
||||
InstallMonado,
|
||||
CloseBuildMonadoModal,
|
||||
}
|
||||
|
||||
impl MainWin {
|
||||
fn get_profile_by_name(&self, name: &String) -> Option<&Profile> {
|
||||
for profile in &self.profiles {
|
||||
if &profile.name == name {
|
||||
return Some(profile);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn get_selected_profile(&self) -> Option<&Profile> {
|
||||
self.get_profile_by_name(&self.config.selected_profile_name)
|
||||
}
|
||||
|
||||
fn start_monado(&mut self) {
|
||||
self.monado_runner.get_mut().terminate();
|
||||
|
||||
let profile = self.get_selected_profile().unwrap();
|
||||
|
||||
self.monado_runner
|
||||
.set(Runner::monado_runner_from_profile(profile.clone()));
|
||||
|
||||
self.monado_runner.get_mut().start();
|
||||
}
|
||||
|
||||
fn get_monado_log(&mut self) {
|
||||
self.monado_log = match self.monado_runner.get_mut().status() {
|
||||
RunnerStatus::Running => self.monado_runner.get_mut().get_output(),
|
||||
RunnerStatus::Stopped => "Monado service inactive".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn install_monado(&mut self) {
|
||||
self.build_monado_log = String::new();
|
||||
let profile = match self.get_selected_profile() {
|
||||
None => {
|
||||
return;
|
||||
}
|
||||
Some(p) => p,
|
||||
};
|
||||
self.build_monado_runner = Some(get_build_monado_runner(profile.clone()));
|
||||
self.build_monado_runner.as_mut().unwrap().start();
|
||||
self.show_build_monado_modal = true;
|
||||
self.build_monado_log = self.build_monado_runner.as_mut().unwrap().get_output();
|
||||
}
|
||||
}
|
||||
|
||||
impl Application for MainWin {
|
||||
type Message = Message;
|
||||
type Flags = ();
|
||||
type Executor = executor::Default;
|
||||
type Theme = Theme;
|
||||
|
||||
fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
|
||||
let profiles = vec![valve_index_profile()];
|
||||
let monado_runner = Cell::new(Runner::monado_runner_from_profile(
|
||||
profiles.get(0).cloned().unwrap(),
|
||||
));
|
||||
(
|
||||
Self {
|
||||
// TODO: load profiles from disk somehow
|
||||
profiles,
|
||||
config: get_config(),
|
||||
debug_search_term: "".into(),
|
||||
monado_runner,
|
||||
monado_active: false,
|
||||
monado_log: "".into(),
|
||||
build_monado_runner: None,
|
||||
show_build_monado_modal: false,
|
||||
build_monado_log: String::new(),
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
return APP_NAME.into();
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Message) -> Command<Message> {
|
||||
match msg {
|
||||
Message::Start => {
|
||||
self.start_monado();
|
||||
}
|
||||
Message::Stop => {
|
||||
self.monado_runner.get_mut().terminate();
|
||||
}
|
||||
Message::ProfileChanged(profile) => {
|
||||
if self.config.selected_profile_name != profile.name {
|
||||
self.config.selected_profile_name = profile.name;
|
||||
save_config(&self.config).expect_dialog("Failed to save config");
|
||||
}
|
||||
}
|
||||
Message::EditProfile => {
|
||||
println!("Edit {}", self.get_selected_profile().unwrap())
|
||||
}
|
||||
Message::DebugViewChanged(nval) => {
|
||||
self.config.debug_view_enabled = nval;
|
||||
save_config(&self.config).expect_dialog("Failed to save config");
|
||||
}
|
||||
Message::DebugSearchChanged(term) => self.debug_search_term = term,
|
||||
Message::InstallMonado => {
|
||||
self.install_monado();
|
||||
}
|
||||
Message::CloseBuildMonadoModal => match &mut self.build_monado_runner {
|
||||
None => {
|
||||
self.show_build_monado_modal = false;
|
||||
}
|
||||
Some(runner) => match runner.status() {
|
||||
RunnerStatus::Running => {}
|
||||
RunnerStatus::Stopped => {
|
||||
self.show_build_monado_modal = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
Message::LogUpdate(_) => {
|
||||
self.monado_active = match self.monado_runner.get_mut().status() {
|
||||
RunnerStatus::Running => true,
|
||||
RunnerStatus::Stopped => false,
|
||||
};
|
||||
self.get_monado_log();
|
||||
if self.show_build_monado_modal {
|
||||
match &mut self.build_monado_runner {
|
||||
None => {}
|
||||
Some(runner) => match runner.status() {
|
||||
RunnerStatus::Running => {
|
||||
self.build_monado_log = runner.get_output();
|
||||
}
|
||||
RunnerStatus::Stopped => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => println!("unhandled"),
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Self::Message> {
|
||||
time::every(Duration::from_millis(500)).map(Message::LogUpdate)
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let debug_view: Element<Message> = match self.config.debug_view_enabled {
|
||||
false => Space::new(Length::Shrink, Length::Shrink).into(),
|
||||
true => {
|
||||
let debug_toolbar = container(row![
|
||||
pick_list(
|
||||
vec![
|
||||
"Debug".to_string(),
|
||||
"Info".into(),
|
||||
"Warning".into(),
|
||||
"Error".into(),
|
||||
],
|
||||
Some("Debug".into()),
|
||||
Message::LogLevelChanged
|
||||
),
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
text_input("Search...", self.debug_search_term.as_str())
|
||||
.on_input(Message::DebugSearchChanged),
|
||||
])
|
||||
.style(Container::Custom(Box::new(BorderedContainer)))
|
||||
.padding(12);
|
||||
|
||||
let view = column![
|
||||
// TODO: tab bar
|
||||
container(
|
||||
scrollable(text(self.monado_log.clone()))
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
)
|
||||
// .style(Container::Custom(Box::new(RoundedBorderedContainer)))
|
||||
.padding(6)
|
||||
.height(Length::Fill),
|
||||
debug_toolbar,
|
||||
];
|
||||
|
||||
row![vseparator(), view]
|
||||
.width(Length::FillPortion(3))
|
||||
.into()
|
||||
}
|
||||
};
|
||||
|
||||
let toolbar = container(
|
||||
row![
|
||||
pick_list(
|
||||
self.profiles.to_vec(),
|
||||
self.get_selected_profile().cloned(),
|
||||
Message::ProfileChanged
|
||||
),
|
||||
button("Edit").on_press(Message::EditProfile),
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
checkbox(
|
||||
"Debug View",
|
||||
self.config.debug_view_enabled,
|
||||
Message::DebugViewChanged
|
||||
),
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(3),
|
||||
)
|
||||
.padding(12)
|
||||
.style(Container::Custom(Box::new(BorderedContainer)));
|
||||
|
||||
let monado_view = column![
|
||||
match self.monado_active {
|
||||
true => button("Stop")
|
||||
.on_press(Message::Stop)
|
||||
.style(Button::Destructive),
|
||||
false => button("Start").on_press(Message::Start),
|
||||
}
|
||||
.padding(Padding::from([6, 24])),
|
||||
scrollable(
|
||||
column![button("DEBUG: install monado").on_press(Message::InstallMonado),]
|
||||
.spacing(6)
|
||||
) // device info goes here
|
||||
]
|
||||
.spacing(12)
|
||||
.height(Length::Fill)
|
||||
.padding(12)
|
||||
.align_items(Alignment::Center);
|
||||
|
||||
let main_view = column![monado_view, toolbar,]
|
||||
.width(Length::FillPortion(2))
|
||||
.height(Length::Fill);
|
||||
|
||||
let win_content = row![main_view, debug_view,];
|
||||
|
||||
Modal::new(self.show_build_monado_modal, win_content, || {
|
||||
Card::new(
|
||||
text("Installing Monado..."),
|
||||
column![scrollable(text(self.build_monado_log.clone())),].spacing(12),
|
||||
)
|
||||
.foot(row![
|
||||
button("Close").on_press(Message::CloseBuildMonadoModal),
|
||||
])
|
||||
.max_width(600.0)
|
||||
.max_height(400.0)
|
||||
.into()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn theme(&self) -> Self::Theme {
|
||||
Theme::Dark
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
pub mod main_win;
|
||||
pub mod widgets;
|
||||
pub mod styles;
|
||||
pub mod app;
|
||||
pub mod main_view;
|
||||
pub mod about_dialog;
|
||||
pub mod debug_view;
|
||||
pub mod build_window;
|
||||
|
|
Loading…
Add table
Reference in a new issue