diff --git a/Cargo.toml b/Cargo.toml index 00d7a1a..986b25d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ dirs = "^3.0.1" gio = "^0" gtk = "^0" rodio = "^0.11.0" +iui = "0.3" diff --git a/src/main.rs b/src/main.rs index 86c18ff..35c86b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,30 +10,35 @@ use std::io::BufReader; use std::path::PathBuf; use std::process::Command; -const SPACING: i32 = 16; - -fn error_popup(message: &str) { - let popup = gtk::Window::new(gtk::WindowType::Toplevel); - popup.set_title("error"); - popup.set_border_width(SPACING as u32); - popup.set_position(gtk::WindowPosition::Center); - popup.set_default_size(256, 64); - - let vertical = gtk::Box::new(Orientation::Vertical, SPACING); - popup.add(&vertical); - - let message = gtk::Label::new(Some(message)); - vertical.add(&message); - - let button_ok = gtk::Button::with_label("OK"); - vertical.add(&button_ok); - - popup.show_all(); - - button_ok.connect_clicked(move |_| unsafe { - popup.destroy(); - }); -} +use iui::prelude::*; +use iui::controls::{Label, Spinbox, Slider, Entry, MultilineEntry, VerticalBox, HorizontalBox, HorizontalSeparator, Group, Spacer}; +use std::rc::Rc; +use std::cell::RefCell; +// +// const SPACING: i32 = 16; +// +// fn error_popup(message: &str) { +// let popup = gtk::Window::new(gtk::WindowType::Toplevel); +// popup.set_title("error"); +// popup.set_border_width(SPACING as u32); +// popup.set_position(gtk::WindowPosition::Center); +// popup.set_default_size(256, 64); +// +// let vertical = gtk::Box::new(Orientation::Vertical, SPACING); +// popup.add(&vertical); +// +// let message = gtk::Label::new(Some(message)); +// vertical.add(&message); +// +// let button_ok = gtk::Button::with_label("OK"); +// vertical.add(&button_ok); +// +// popup.show_all(); +// +// button_ok.connect_clicked(move |_| unsafe { +// popup.destroy(); +// }); +// } fn get_data_dir() -> PathBuf { let mut data_dir = dirs::data_dir().expect("Couldn't find user data directory"); @@ -46,115 +51,220 @@ fn get_data_dir() -> PathBuf { data_dir } +// +// fn build_ui(application: >k::Application) { +// let window = gtk::ApplicationWindow::new(application); +// +// window.set_title("lull"); +// window.set_border_width(SPACING as u32); +// window.set_position(gtk::WindowPosition::Center); +// window.set_default_size(256, 256); +// +// let vertical = gtk::Box::new(Orientation::Vertical, SPACING); +// vertical.set_homogeneous(true); +// +// window.add(&vertical); +// +// let device = rodio::default_output_device().unwrap(); +// +// let paths = std::fs::read_dir(get_data_dir()) +// .expect("Couldn't read from lull data directory"); +// +// for path in paths { +// let path = path.unwrap().path(); +// let name: &str = path.file_stem().unwrap().to_str().unwrap(); +// +// let file = File::open(&path) +// .expect("Couldn't open audio file"); +// +// let source = rodio::Decoder::new( +// BufReader::new(file) +// ); +// +// if source.is_err() { +// error_popup(&format!( +// "Couldn't parse file {}. \n{}.", +// path.to_str().unwrap(), +// source.err().unwrap() +// )); +// continue; +// } +// +// let source = source.unwrap().repeat_infinite(); +// +// let sink = Sink::new(&device); +// sink.append(source); +// sink.pause(); +// +// let row = gtk::Box::new(Orientation::Horizontal, SPACING); +// row.set_homogeneous(true); +// +// let label = gtk::Label::new(Some(name)); +// row.add(&label); +// +// let adjustment = gtk::Adjustment::new( +// 0.0, +// 0.0, +// 1.0, +// 0.0, +// 0.0, +// 0.0 +// ); +// +// let slider = gtk::Scale::new( +// Orientation::Horizontal, +// Some(&adjustment) +// ); +// +// slider.set_draw_value(false); +// +// slider.connect_value_changed(move |scale| { +// let volume = scale.get_value(); +// +// if volume == 0. { +// sink.pause(); +// } else { +// sink.play(); +// sink.set_volume(volume as f32); +// } +// }); +// +// row.add(&slider); +// +// vertical.add(&row); +// } +// +// let row_add = gtk::Box::new(Orientation::Horizontal, SPACING); +// row_add.set_homogeneous(true); +// +// let button_manage_sounds = gtk::Button::with_label("manage sounds"); +// +// button_manage_sounds.connect_clicked(|_| { +// let mut file_manager = Command::new("xdg-open"); +// file_manager.arg(get_data_dir()); +// file_manager.output().unwrap(); +// }); +// +// row_add.add(&button_manage_sounds); +// vertical.add(&row_add); +// +// window.show_all(); +// } -fn build_ui(application: >k::Application) { - let window = gtk::ApplicationWindow::new(application); - - window.set_title("lull"); - window.set_border_width(SPACING as u32); - window.set_position(gtk::WindowPosition::Center); - window.set_default_size(256, 256); - - let vertical = gtk::Box::new(Orientation::Vertical, SPACING); - vertical.set_homogeneous(true); - - window.add(&vertical); - - let device = rodio::default_output_device().unwrap(); - - let paths = std::fs::read_dir(get_data_dir()) - .expect("Couldn't read from lull data directory"); - - for path in paths { - let path = path.unwrap().path(); - let name: &str = path.file_stem().unwrap().to_str().unwrap(); - - let file = File::open(&path) - .expect("Couldn't open audio file"); - - let source = rodio::Decoder::new( - BufReader::new(file) - ); - - if source.is_err() { - error_popup(&format!( - "Couldn't parse file {}. \n{}.", - path.to_str().unwrap(), - source.err().unwrap() - )); - continue; - } - - let source = source.unwrap().repeat_infinite(); - - let sink = Sink::new(&device); - sink.append(source); - sink.pause(); - - let row = gtk::Box::new(Orientation::Horizontal, SPACING); - row.set_homogeneous(true); - - let label = gtk::Label::new(Some(name)); - row.add(&label); - - let adjustment = gtk::Adjustment::new( - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0 - ); - - let slider = gtk::Scale::new( - Orientation::Horizontal, - Some(&adjustment) - ); - - slider.set_draw_value(false); - - slider.connect_value_changed(move |scale| { - let volume = scale.get_value(); - - if volume == 0. { - sink.pause(); - } else { - sink.play(); - sink.set_volume(volume as f32); - } - }); - - row.add(&slider); - - vertical.add(&row); - } - - let row_add = gtk::Box::new(Orientation::Horizontal, SPACING); - row_add.set_homogeneous(true); - - let button_manage_sounds = gtk::Button::with_label("manage sounds"); - - button_manage_sounds.connect_clicked(|_| { - let mut file_manager = Command::new("xdg-open"); - file_manager.arg(get_data_dir()); - file_manager.output().unwrap(); - }); - - row_add.add(&button_manage_sounds); - vertical.add(&row_add); - - window.show_all(); +/// This struct will hold the values that multiple callbacks will need to access. +struct State { + slider_val: i64, + spinner_val: i64, + entry_val: String, + multi_val: String, } fn main() { - let application = gtk::Application::new( - Some("dev.tinybird.max.lull"), - Default::default() - ).expect("Initialization failed..."); + // Initialize the UI framework. + let ui = UI::init().unwrap(); - application.connect_activate(|app| { - build_ui(app); + // Initialize the state of the application. + let state = Rc::new(RefCell::new(State { slider_val: 0, spinner_val: 0, entry_val: "".into(), multi_val: "".into() })); + + // Set up the inputs for the application. + // While it's not necessary to create a block for this, it makes the code a lot easier + // to read; the indentation presents a visual cue informing the reader that these + // statements are related. + let (input_group, mut slider, mut spinner, mut entry, mut multi) = { + // The group will hold all the inputs + let mut input_group = Group::new(&ui, "Inputs"); + // The vertical box arranges the inputs within the groups + let mut input_vbox = VerticalBox::new(&ui); + input_vbox.set_padded(&ui, true); + // Numerical inputs + let slider = Slider::new(&ui, 1, 100); + let spinner = Spinbox::new(&ui, 1, 100); + let entry = Entry::new(&ui); + let multi = MultilineEntry::new(&ui); + // Add everything in hierarchy + // Note the reverse order here. Again, it's not necessary, but it improves + // readability. + input_vbox.append(&ui, slider.clone(), LayoutStrategy::Compact); + input_vbox.append(&ui, spinner.clone(), LayoutStrategy::Compact); + input_vbox.append(&ui, Spacer::new(&ui), LayoutStrategy::Compact); + input_vbox.append(&ui, HorizontalSeparator::new(&ui), LayoutStrategy::Compact); + input_vbox.append(&ui, Spacer::new(&ui), LayoutStrategy::Compact); + input_vbox.append(&ui, entry.clone(), LayoutStrategy::Compact); + input_vbox.append(&ui, multi.clone(), LayoutStrategy::Stretchy); + input_group.set_child(&ui, input_vbox); + (input_group, slider, spinner, entry, multi) + }; + + // Set up the outputs for the application. Organization is very similar to the + // previous setup. + let (output_group, add_label, sub_label, text_label, bigtext_label) = { + let mut output_group = Group::new(&ui, "Outputs"); + let mut output_vbox = VerticalBox::new(&ui); + let add_label = Label::new(&ui, ""); + let sub_label = Label::new(&ui, ""); + let text_label = Label::new(&ui, ""); + let bigtext_label = Label::new(&ui, ""); + output_vbox.append(&ui, add_label.clone(), LayoutStrategy::Compact); + output_vbox.append(&ui, sub_label.clone(), LayoutStrategy::Compact); + output_vbox.append(&ui, text_label.clone(), LayoutStrategy::Compact); + output_vbox.append(&ui, bigtext_label.clone(), LayoutStrategy::Stretchy); + output_group.set_child(&ui, output_vbox); + (output_group, add_label, sub_label, text_label, bigtext_label) + }; + + // This horizontal box will arrange the two groups of controls. + let mut hbox = HorizontalBox::new(&ui); + hbox.append(&ui, input_group, LayoutStrategy::Stretchy); + hbox.append(&ui, output_group, LayoutStrategy::Stretchy); + + // The window allows all constituent components to be displayed. + let mut window = Window::new(&ui, "Input Output Test", 300, 150, WindowType::NoMenubar); + window.set_child(&ui, hbox); + window.show(&ui); + + // These on_changed functions allow updating the application state when a + // control changes its value. + + slider.on_changed(&ui, { + let state = state.clone(); + move |val| { state.borrow_mut().slider_val = val; } }); - application.run(&args().collect::>()); + spinner.on_changed(&ui, { + let state = state.clone(); + move |val| { state.borrow_mut().spinner_val = val; } + }); + + entry.on_changed(&ui, { + let state = state.clone(); + move |val| { state.borrow_mut().entry_val = val; } + }); + + multi.on_changed(&ui, { + let state = state.clone(); + move |val| { state.borrow_mut().multi_val = val; } + }); + + + // Rather than just invoking ui.run(), using EventLoop gives a lot more control + // over the user interface event loop. + // Here, the on_tick() callback is used to update the view against the state. + let mut event_loop = ui.event_loop(); + event_loop.on_tick(&ui, { + let ui = ui.clone(); + let mut add_label = add_label.clone(); + let mut sub_label = sub_label.clone(); + let mut text_label = text_label.clone(); + let mut bigtext_label = bigtext_label.clone(); + move || { + let state = state.borrow(); + + // Update all the labels + add_label.set_text(&ui, &format!("Added: {}", state.slider_val + state.spinner_val)); + sub_label.set_text(&ui, &format!("Subtracted: {}", state.slider_val - state.spinner_val)); + text_label.set_text(&ui, &format!("Text: {}", state.entry_val)); + bigtext_label.set_text(&ui, &format!("Multiline Text: {}", state.multi_val)); + } + }); + event_loop.run(&ui); }