├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── examples ├── add_data.rs ├── demo.rs ├── demo_mod │ ├── listeners.rs │ ├── mod.rs │ └── models.rs ├── hello_world.rs ├── image_viewer.rs ├── image_viewer_mod │ ├── listeners.rs │ ├── mod.rs │ └── models.rs ├── login.rs └── timer.rs ├── rustfmt.toml └── src ├── lib.rs ├── utils ├── event.rs ├── icon.rs ├── mod.rs ├── pixmap.rs ├── style.rs └── theme.rs ├── widgets ├── button.rs ├── checkbox.rs ├── combo.rs ├── container.rs ├── image.rs ├── label.rs ├── menubar.rs ├── mod.rs ├── progressbar.rs ├── radio.rs ├── range.rs ├── tabs.rs ├── textinput.rs └── widget.rs └── www ├── app ├── app.js ├── app.scss └── morphdom.min.js ├── icons ├── Breeze │ ├── Bell.svg │ ├── Bookmark.svg │ ├── Check.svg │ ├── Clock.svg │ ├── Down.svg │ ├── Edit.svg │ ├── Heart.svg │ ├── Home.svg │ ├── Left.svg │ ├── Lock.svg │ ├── Minus.svg │ ├── Plus.svg │ ├── Right.svg │ ├── Save.svg │ ├── Star.svg │ ├── Trash.svg │ ├── Unlock.svg │ ├── Up.svg │ ├── ZoomIn.svg │ └── ZoomOut.svg └── Default │ ├── Down.svg │ └── Plus.svg └── themes ├── Adwaita.scss ├── Breeze.scss ├── Default.scss ├── Fluent.scss └── OSX.scss /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "neutrino" 3 | version = "0.3.1" 4 | authors = ["Alexis Lozano "] 5 | edition = "2018" 6 | build = "build.rs" 7 | description = "A GUI frontend in Rust based on web-view" 8 | repository = "https://github.com/alexislozano/neutrino/" 9 | readme = "README.md" 10 | license = "MIT" 11 | categories = ["gui"] 12 | keywords = ["gui", "desktop", "web", "mvc"] 13 | 14 | [dependencies] 15 | web-view = "0.4.1" 16 | strfmt = "0.1.6" 17 | base64 = "0.10.1" 18 | json = "0.11.15" 19 | rsass = "0.11.0" 20 | html-minifier = "1.1.14" 21 | 22 | [build-dependencies] 23 | rsass = "0.11.0" 24 | base64 = "0.10.1" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 alexislozano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neutrino 2 | 3 | **I am not working anymore on this project. If you want to become a maintainer of neutrino, please answer to [this issue](https://github.com/alexislozano/neutrino/issues/87).** 4 | 5 | ## Preamble 6 | 7 | [Docs](https://docs.rs/neutrino) | 8 | [Repo](https://github.com/alexislozano/neutrino) | 9 | [Wiki](https://github.com/alexislozano/neutrino/wiki) | 10 | [Crate](https://crates.io/crates/neutrino) 11 | 12 | Neutrino is a MVC GUI framework written in Rust. It lets users create GUI 13 | applications by positioning widgets on a window and by handling events. 14 | Neutrino is based on the [web-view](https://crates.io/crates/web-view) crate 15 | provided by Boscop. As such, Neutrino renders the application using web 16 | technologies as HTML and CSS. 17 | As it is based on web-view, Neutrino does not embed a whole web browser. So 18 | don't worry, due to the very lightweight footprint of web-view, you won't 19 | have to buy more memory for your computer. 20 | 21 | ## Install 22 | 23 | In order to use Neutrino, you will have to use cargo. Just add the following 24 | line to your `Cargo.toml` and you'll be done : 25 | 26 | ```text 27 | neutrino = "" 28 | ``` 29 | 30 | On Linux, you'll have to install webkit2gtk's development library. For example, 31 | in Ubuntu or Debian: 32 | ``` 33 | sudo apt install -y libwebkit2gtk-4.0-dev 34 | ``` 35 | 36 | ## Examples 37 | 38 | ![](https://raw.githubusercontent.com/wiki/alexislozano/neutrino/images/image_viewer/3.png) 39 | 40 | ![](https://raw.githubusercontent.com/wiki/alexislozano/neutrino/images/styling/3.png) 41 | 42 | ![](https://raw.githubusercontent.com/wiki/alexislozano/neutrino/images/styling/4.png) 43 | 44 | ![](https://raw.githubusercontent.com/wiki/alexislozano/neutrino/images/styling/5.png) 45 | 46 | ![](https://raw.githubusercontent.com/wiki/alexislozano/neutrino/images/styling/6.png) 47 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use base64::encode; 2 | use rsass::{compile_scss_file, OutputStyle}; 3 | use std::env; 4 | use std::fs; 5 | use std::str; 6 | 7 | fn main() { 8 | let out_dir = &env::var("OUT_DIR").unwrap(); 9 | 10 | // Commons 11 | scss("app", out_dir); 12 | 13 | // Themes 14 | themes(out_dir); 15 | 16 | // Icons 17 | icons(out_dir); 18 | } 19 | 20 | fn scss(name: &str, out_dir: &str) { 21 | let path = format!("src/www/{}/{}.scss", name, name); 22 | let out = format!("{}/{}.css", out_dir, name); 23 | let css = 24 | compile_scss_file(path.as_ref(), OutputStyle::Compressed).unwrap(); 25 | fs::write(out, css).unwrap(); 26 | } 27 | 28 | fn themes(out_dir: &str) { 29 | let mut enum_data = r#" 30 | /// # A theme 31 | pub enum Theme { 32 | "# 33 | .to_string(); 34 | let mut impl_data = r#" 35 | impl Theme { 36 | /// Get a string containing the CSS defining the theme 37 | pub fn css(&self) -> &str { 38 | match self { 39 | "# 40 | .to_string(); 41 | 42 | fs::create_dir_all(format!("{}/themes", out_dir)).unwrap(); 43 | match fs::read_dir("src/www/themes") { 44 | Err(e) => panic!(e), 45 | Ok(entries) => { 46 | for entry in entries { 47 | let path = entry.unwrap().path(); 48 | let filestem = path 49 | .clone() 50 | .file_stem() 51 | .unwrap() 52 | .to_str() 53 | .unwrap() 54 | .to_string(); 55 | let css = 56 | compile_scss_file(path.as_ref(), OutputStyle::Compressed) 57 | .unwrap(); 58 | let css_str = str::from_utf8(&css).unwrap(); 59 | enum_data.push_str(&format!(r#"{},"#, &filestem)); 60 | impl_data.push_str(&format!( 61 | r##"Theme::{} => r#"{}"#,"##, 62 | &filestem, css_str 63 | )) 64 | } 65 | } 66 | }; 67 | 68 | enum_data.push_str("}"); 69 | impl_data.push_str("}}}"); 70 | fs::write(format!("{}/themes/enum.rs", out_dir), enum_data).unwrap(); 71 | fs::write(format!("{}/themes/impl.rs", out_dir), impl_data).unwrap(); 72 | } 73 | 74 | fn icons(out_dir: &str) { 75 | let mut enum_data = r#""#.to_string(); 76 | let mut impl_data = r#""#.to_string(); 77 | 78 | fs::create_dir_all(format!("{}/icons/", out_dir)).unwrap(); 79 | match fs::read_dir("src/www/icons/") { 80 | Err(e) => panic!(e), 81 | Ok(dirs) => { 82 | for dir in dirs { 83 | let path = dir.unwrap().path(); 84 | let dirstem = path 85 | .clone() 86 | .file_stem() 87 | .unwrap() 88 | .to_str() 89 | .unwrap() 90 | .to_string(); 91 | 92 | enum_data.push_str(&format!( 93 | r#" 94 | /// # The {} icon set 95 | pub enum {}Icon {{ 96 | "#, 97 | &dirstem, &dirstem 98 | )); 99 | impl_data 100 | .push_str(&format!(r#"impl Icon for {}Icon {{"#, dirstem)); 101 | 102 | let mut impl_function_data = r#"fn data(&self) -> String { 103 | match self { 104 | "# 105 | .to_string(); 106 | let mut impl_function_extension = 107 | r#"fn extension(&self) -> String { 108 | match self { 109 | "# 110 | .to_string(); 111 | 112 | match fs::read_dir(&path) { 113 | Err(e) => panic!(e), 114 | Ok(entries) => { 115 | for entry in entries { 116 | let path = entry.unwrap().path(); 117 | let path1 = path.clone(); 118 | let path2 = path.clone(); 119 | 120 | let filestem = path 121 | .file_stem() 122 | .unwrap() 123 | .to_str() 124 | .unwrap() 125 | .to_string(); 126 | 127 | enum_data.push_str(&format!(r#"{},"#, &filestem)); 128 | 129 | let extension = 130 | path1.extension().unwrap().to_str().unwrap(); 131 | let data = encode( 132 | &String::from_utf8_lossy( 133 | &fs::read(path2).unwrap(), 134 | ) 135 | .replace("\n", ""), 136 | ); 137 | 138 | impl_function_extension.push_str(&format!( 139 | r#"{}Icon::{} => "{}".to_string(),"#, 140 | dirstem, filestem, &extension 141 | )); 142 | 143 | impl_function_data.push_str(&format!( 144 | r#"{}Icon::{} => "{}".to_string(),"#, 145 | dirstem, filestem, &data 146 | )); 147 | } 148 | } 149 | }; 150 | 151 | enum_data.push_str(r#"}"#); 152 | 153 | impl_function_data.push_str(r#"}}"#); 154 | impl_function_extension.push_str(r#"}}"#); 155 | 156 | impl_data.push_str(&impl_function_data); 157 | impl_data.push_str(&impl_function_extension); 158 | impl_data.push_str(r#"}"#); 159 | } 160 | } 161 | }; 162 | fs::write(format!("{}/icons/enum.rs", out_dir), enum_data).unwrap(); 163 | fs::write(format!("{}/icons/impl.rs", out_dir), impl_data).unwrap(); 164 | } 165 | -------------------------------------------------------------------------------- /examples/add_data.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use neutrino::widgets::button::{Button, ButtonListener, ButtonState}; 5 | use neutrino::{App, Window}; 6 | 7 | struct Counter { 8 | value: u8, 9 | } 10 | 11 | impl Counter { 12 | fn new() -> Self { 13 | Self { value: 0 } 14 | } 15 | 16 | fn value(&self) -> u8 { 17 | self.value 18 | } 19 | 20 | fn increment(&mut self) { 21 | self.value += 1; 22 | } 23 | } 24 | 25 | struct MyButtonListener { 26 | counter: Rc>, 27 | } 28 | 29 | impl MyButtonListener { 30 | fn new(counter: Rc>) -> Self { 31 | Self { counter } 32 | } 33 | } 34 | 35 | impl ButtonListener for MyButtonListener { 36 | fn on_change(&self, _state: &ButtonState) { 37 | self.counter.borrow_mut().increment(); 38 | } 39 | 40 | fn on_update(&self, state: &mut ButtonState) { 41 | state.set_text(&self.counter.borrow().value().to_string()); 42 | } 43 | } 44 | 45 | fn main() { 46 | let counter = Rc::new(RefCell::new(Counter::new())); 47 | 48 | let listener = MyButtonListener::new(Rc::clone(&counter)); 49 | 50 | let mut button = Button::new("my_button"); 51 | button.set_text("0"); 52 | button.set_listener(Box::new(listener)); 53 | 54 | let mut window = Window::new(); 55 | window.set_title("Add data"); 56 | window.set_size(320, 240); 57 | window.set_child(Box::new(button)); 58 | window.set_debug(); 59 | 60 | App::run(window); 61 | } 62 | -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | use neutrino::utils::event::Key; 2 | use neutrino::utils::icon::BreezeIcon; 3 | use neutrino::utils::theme::Theme; 4 | use neutrino::widgets::button::Button; 5 | use neutrino::widgets::checkbox::CheckBox; 6 | use neutrino::widgets::combo::Combo; 7 | use neutrino::widgets::container::{Alignment, Container, Direction}; 8 | use neutrino::widgets::label::Label; 9 | use neutrino::widgets::menubar::{MenuBar, MenuFunction, MenuItem}; 10 | use neutrino::widgets::progressbar::ProgressBar; 11 | use neutrino::widgets::radio::Radio; 12 | use neutrino::widgets::range::Range; 13 | use neutrino::widgets::tabs::Tabs; 14 | use neutrino::widgets::textinput::TextInput; 15 | use neutrino::{App, Window}; 16 | 17 | mod demo_mod; 18 | 19 | use demo_mod::listeners::{ 20 | MyButtonListener, MyCheckBoxDisabledListener, MyCheckBoxListener, 21 | MyComboListener, MyLabelListener, MyMenuBarListener, MyProgressBarListener, 22 | MyRadioListener, MyRangeListener, MyTabsListener, MyTextInputListener, 23 | MyWindowListener, 24 | }; 25 | use demo_mod::models::{Panes, State}; 26 | 27 | use std::cell::RefCell; 28 | use std::env; 29 | use std::rc::Rc; 30 | 31 | fn main() { 32 | let panes = Rc::new(RefCell::new(Panes::new())); 33 | 34 | let state = Rc::new(RefCell::new(State::new())); 35 | 36 | let textinput_listener = MyTextInputListener::new(Rc::clone(&state)); 37 | 38 | let mut textinput1 = TextInput::new("input1"); 39 | textinput1.set_listener(Box::new(textinput_listener)); 40 | textinput1.set_value("0"); 41 | textinput1.set_placeholder("0-100"); 42 | textinput1.set_size(4); 43 | 44 | let button_listener = MyButtonListener::new(Rc::clone(&state)); 45 | 46 | let mut button1 = Button::new("button1"); 47 | button1.set_text("Button"); 48 | button1.set_stretched(); 49 | button1.set_icon(Box::new(BreezeIcon::Check)); 50 | button1.set_listener(Box::new(button_listener)); 51 | 52 | let progressbar_listener = MyProgressBarListener::new(Rc::clone(&state)); 53 | 54 | let mut progressbar1 = ProgressBar::new("progressbar1"); 55 | progressbar1.set_listener(Box::new(progressbar_listener)); 56 | progressbar1.set_value(0); 57 | progressbar1.set_stretched(); 58 | 59 | let label_listener = MyLabelListener::new(Rc::clone(&state)); 60 | 61 | let mut label1 = Label::new("label1"); 62 | label1.set_listener(Box::new(label_listener)); 63 | label1.set_text("0%"); 64 | 65 | let checkbox_listener = MyCheckBoxListener::new(Rc::clone(&state)); 66 | 67 | let mut checkbox1 = CheckBox::new("checkbox1"); 68 | checkbox1.set_text("Checkbox"); 69 | checkbox1.set_checked(); 70 | checkbox1.set_listener(Box::new(checkbox_listener)); 71 | 72 | let checkbox_disabled_listener = 73 | MyCheckBoxDisabledListener::new(Rc::clone(&state)); 74 | 75 | let mut checkbox_disabled = CheckBox::new("checkbox_disabled"); 76 | checkbox_disabled.set_text("Disabled"); 77 | checkbox_disabled.set_listener(Box::new(checkbox_disabled_listener)); 78 | 79 | let radio_listener = MyRadioListener::new(Rc::clone(&state)); 80 | 81 | let mut radio1 = Radio::new("radio1"); 82 | radio1.set_choices(vec!["Radio Button", "Radio Button"]); 83 | radio1.set_selected(0); 84 | radio1.set_listener(Box::new(radio_listener)); 85 | 86 | let combo_listener = MyComboListener::new(Rc::clone(&state)); 87 | 88 | let mut combo1 = Combo::new("combo1"); 89 | combo1.set_choices(vec!["Combo Box", "Jumbo Fox"]); 90 | combo1.set_selected(0); 91 | combo1.set_listener(Box::new(combo_listener)); 92 | 93 | let range_listener = MyRangeListener::new(Rc::clone(&state)); 94 | 95 | let mut range1 = Range::new("range1"); 96 | range1.set_listener(Box::new(range_listener)); 97 | range1.set_min(0); 98 | range1.set_max(100); 99 | range1.set_value(0); 100 | range1.set_stretched(); 101 | 102 | let mut container1 = Container::new("container1"); 103 | container1.set_direction(Direction::Vertical); 104 | container1.set_stretched(); 105 | container1.add(Box::new(checkbox1)); 106 | container1.add(Box::new(radio1)); 107 | 108 | let mut container2 = Container::new("container2"); 109 | container2.set_direction(Direction::Horizontal); 110 | container2.set_alignment(Alignment::Center); 111 | container2.add(Box::new(button1)); 112 | container2.add(Box::new(textinput1)); 113 | 114 | let mut container3 = Container::new("container3"); 115 | container3.set_direction(Direction::Vertical); 116 | container3.set_stretched(); 117 | container3.add(Box::new(combo1)); 118 | container3.add(Box::new(container2)); 119 | 120 | let mut container4 = Container::new("container4"); 121 | container4.set_direction(Direction::Horizontal); 122 | container4.add(Box::new(container1)); 123 | container4.add(Box::new(container3)); 124 | 125 | let mut container5 = Container::new("container5"); 126 | container5.set_direction(Direction::Horizontal); 127 | container5.set_alignment(Alignment::Center); 128 | container5.add(Box::new(range1)); 129 | container5.add(Box::new(progressbar1)); 130 | container5.add(Box::new(label1)); 131 | 132 | let mut container6 = Container::new("container6"); 133 | container6.set_direction(Direction::Vertical); 134 | container6.add(Box::new(container4)); 135 | container6.add(Box::new(container5)); 136 | container6.add(Box::new(checkbox_disabled)); 137 | 138 | let mut label2 = Label::new("label2"); 139 | label2.set_text("This is Tab 2."); 140 | 141 | let mut label3 = Label::new("label3"); 142 | label3.set_unselectable(); 143 | label3.set_text("This label text is unselectable"); 144 | 145 | let mut container7 = Container::new("container7"); 146 | container7.set_direction(Direction::Vertical); 147 | container7.add(Box::new(label2)); 148 | container7.add(Box::new(label3)); 149 | 150 | let mut label4 = Label::new("label4"); 151 | label4.set_text("This is Tab 3"); 152 | 153 | let tabs_listener = MyTabsListener::new(Rc::clone(&panes)); 154 | 155 | let mut tabs1 = Tabs::new("tabs1"); 156 | tabs1.set_selected(0); 157 | tabs1.set_listener(Box::new(tabs_listener)); 158 | tabs1.add("Tab 1", Box::new(container6)); 159 | tabs1.add("Tab 2", Box::new(container7)); 160 | tabs1.add("Tab 3", Box::new(label4)); 161 | 162 | let mut quitter = MenuFunction::new("Exit"); 163 | quitter.set_shortcut("Ctrl-Q"); 164 | 165 | let mut fichier = MenuItem::new("File", Key::F, 0); 166 | fichier.add(quitter); 167 | 168 | let mut onglet1 = MenuFunction::new("Tab 1"); 169 | onglet1.set_shortcut("Ctrl-1"); 170 | 171 | let mut onglet2 = MenuFunction::new("Tab 2"); 172 | onglet2.set_shortcut("Ctrl-2"); 173 | 174 | let mut onglet3 = MenuFunction::new("Tab 3"); 175 | onglet3.set_shortcut("Ctrl-3"); 176 | 177 | let mut onglets = MenuItem::new("Tabs", Key::T, 0); 178 | onglets.add(onglet1); 179 | onglets.add(onglet2); 180 | onglets.add(onglet3); 181 | 182 | let menubar_listener = MyMenuBarListener::new(Rc::clone(&panes)); 183 | 184 | let mut menu_bar = MenuBar::new(); 185 | menu_bar.set_listener(Box::new(menubar_listener)); 186 | menu_bar.add(fichier); 187 | menu_bar.add(onglets); 188 | 189 | let app_listener = MyWindowListener::new(Rc::clone(&panes)); 190 | 191 | let mut window = Window::new(); 192 | window.set_title("Demo"); 193 | window.set_size(440, 260); 194 | window.set_resizable(); 195 | window.set_child(Box::new(tabs1)); 196 | window.set_menubar(menu_bar); 197 | window.set_listener(Box::new(app_listener)); 198 | window.set_debug(); 199 | 200 | let args: Vec = env::args().collect(); 201 | 202 | window.set_theme(if args.len() == 2 { 203 | match args[1].as_str() { 204 | "adwaita" => Theme::Adwaita, 205 | "breeze" => Theme::Breeze, 206 | "fluent" => Theme::Fluent, 207 | "osx" => Theme::OSX, 208 | _ => Theme::Default, 209 | } 210 | } else { 211 | Theme::Default 212 | }); 213 | 214 | App::run(window); 215 | } 216 | -------------------------------------------------------------------------------- /examples/demo_mod/listeners.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashSet; 3 | use std::rc::Rc; 4 | 5 | use neutrino::utils::event::Key; 6 | use neutrino::widgets::button::{ButtonListener, ButtonState}; 7 | use neutrino::widgets::checkbox::{CheckBoxListener, CheckBoxState}; 8 | use neutrino::widgets::combo::{ComboListener, ComboState}; 9 | use neutrino::widgets::label::{LabelListener, LabelState}; 10 | use neutrino::widgets::menubar::{MenuBarListener, MenuBarState}; 11 | use neutrino::widgets::progressbar::{ProgressBarListener, ProgressBarState}; 12 | use neutrino::widgets::radio::{RadioListener, RadioState}; 13 | use neutrino::widgets::range::{RangeListener, RangeState}; 14 | use neutrino::widgets::tabs::{TabsListener, TabsState}; 15 | use neutrino::widgets::textinput::{TextInputListener, TextInputState}; 16 | use neutrino::WindowListener; 17 | 18 | use super::models::{Panes, State}; 19 | 20 | /* 21 | Window listener: waits for menu shortcuts 22 | */ 23 | pub struct MyWindowListener { 24 | panes: Rc>, 25 | } 26 | 27 | impl MyWindowListener { 28 | pub fn new(panes: Rc>) -> Self { 29 | Self { panes } 30 | } 31 | } 32 | 33 | impl WindowListener for MyWindowListener { 34 | fn on_keys(&self, keys: HashSet) { 35 | if keys.contains(&Key::Control) { 36 | if keys.contains(&Key::Num1) { 37 | self.panes.borrow_mut().set_value(0); 38 | } else if keys.contains(&Key::Num2) { 39 | self.panes.borrow_mut().set_value(1); 40 | } else if keys.contains(&Key::Num3) { 41 | self.panes.borrow_mut().set_value(2); 42 | } else if keys.contains(&Key::Q) { 43 | std::process::exit(0); 44 | } 45 | } 46 | } 47 | 48 | fn on_tick(&self) {} 49 | } 50 | 51 | /* 52 | Tabs Listener: change current tab when user clicks on a tab label, 53 | on a menu item or uses a shortcut 54 | */ 55 | pub struct MyTabsListener { 56 | panes: Rc>, 57 | } 58 | 59 | impl MyTabsListener { 60 | pub fn new(panes: Rc>) -> Self { 61 | Self { panes } 62 | } 63 | } 64 | 65 | impl TabsListener for MyTabsListener { 66 | fn on_update(&self, state: &mut TabsState) { 67 | state.set_selected(u32::from(self.panes.borrow().value())); 68 | } 69 | 70 | fn on_change(&self, state: &TabsState) { 71 | self.panes.borrow_mut().set_value(state.selected() as u8); 72 | } 73 | } 74 | 75 | /* Menu Bar Listener: waits for the user to select a menu item */ 76 | pub struct MyMenuBarListener { 77 | panes: Rc>, 78 | } 79 | 80 | impl MyMenuBarListener { 81 | pub fn new(panes: Rc>) -> Self { 82 | Self { panes } 83 | } 84 | } 85 | 86 | impl MenuBarListener for MyMenuBarListener { 87 | fn on_change(&self, state: &MenuBarState) { 88 | match state.selected_item() { 89 | None => (), 90 | Some(selected_item) => { 91 | if selected_item == 0 { 92 | std::process::exit(0); 93 | } else if selected_item == 1 { 94 | match state.selected_function() { 95 | None => (), 96 | Some(selected_function) => { 97 | self.panes 98 | .borrow_mut() 99 | .set_value(selected_function as u8); 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | /* Range Listener: update State when the user scroll the Range widget */ 109 | pub struct MyRangeListener { 110 | state: Rc>, 111 | } 112 | 113 | impl MyRangeListener { 114 | pub fn new(state: Rc>) -> Self { 115 | Self { state } 116 | } 117 | } 118 | 119 | impl RangeListener for MyRangeListener { 120 | fn on_update(&self, state: &mut RangeState) { 121 | state.set_value(self.state.borrow().range()); 122 | state.set_disabled(self.state.borrow().disabled()); 123 | } 124 | fn on_change(&self, state: &RangeState) { 125 | self.state.borrow_mut().set_range(state.value()); 126 | } 127 | } 128 | 129 | /* Progress Bar Listener: update the Progress Bar value to the current 130 | State value*/ 131 | pub struct MyProgressBarListener { 132 | state: Rc>, 133 | } 134 | 135 | impl MyProgressBarListener { 136 | pub fn new(state: Rc>) -> Self { 137 | Self { state } 138 | } 139 | } 140 | 141 | impl ProgressBarListener for MyProgressBarListener { 142 | fn on_update(&self, state: &mut ProgressBarState) { 143 | state.set_value(self.state.borrow().range()); 144 | } 145 | } 146 | 147 | /* Label Listenr: update the Label text to show the current State value, 148 | formatted as a percent */ 149 | pub struct MyLabelListener { 150 | state: Rc>, 151 | } 152 | 153 | impl MyLabelListener { 154 | pub fn new(state: Rc>) -> Self { 155 | Self { state } 156 | } 157 | } 158 | 159 | impl LabelListener for MyLabelListener { 160 | fn on_update(&self, state: &mut LabelState) { 161 | let text = format!("{}%", self.state.borrow().range()); 162 | state.set_text(&text); 163 | } 164 | } 165 | 166 | /* Text Input Listener: update the TextInput value to the 167 | current State value or set the State when the user 168 | changes the TextInput value */ 169 | pub struct MyTextInputListener { 170 | state: Rc>, 171 | } 172 | 173 | impl MyTextInputListener { 174 | pub fn new(state: Rc>) -> Self { 175 | Self { state } 176 | } 177 | } 178 | 179 | impl TextInputListener for MyTextInputListener { 180 | fn on_update(&self, state: &mut TextInputState) { 181 | state.set_value(&self.state.borrow().range().to_string()); 182 | state.set_disabled(self.state.borrow().disabled()); 183 | } 184 | fn on_change(&self, state: &TextInputState) { 185 | self.state 186 | .borrow_mut() 187 | .set_range(state.value().parse().unwrap_or(0)); 188 | } 189 | } 190 | 191 | pub struct MyButtonListener { 192 | state: Rc>, 193 | } 194 | 195 | impl MyButtonListener { 196 | pub fn new(state: Rc>) -> Self { 197 | Self { state } 198 | } 199 | } 200 | 201 | impl ButtonListener for MyButtonListener { 202 | fn on_change(&self, _state: &ButtonState) {} 203 | 204 | fn on_update(&self, state: &mut ButtonState) { 205 | state.set_disabled(self.state.borrow().disabled()); 206 | } 207 | } 208 | 209 | pub struct MyComboListener { 210 | state: Rc>, 211 | } 212 | 213 | impl MyComboListener { 214 | pub fn new(state: Rc>) -> Self { 215 | Self { state } 216 | } 217 | } 218 | 219 | impl ComboListener for MyComboListener { 220 | fn on_change(&self, _state: &ComboState) {} 221 | 222 | fn on_update(&self, state: &mut ComboState) { 223 | state.set_disabled(self.state.borrow().disabled()); 224 | } 225 | } 226 | 227 | pub struct MyRadioListener { 228 | state: Rc>, 229 | } 230 | 231 | impl MyRadioListener { 232 | pub fn new(state: Rc>) -> Self { 233 | Self { state } 234 | } 235 | } 236 | 237 | impl RadioListener for MyRadioListener { 238 | fn on_change(&self, _state: &RadioState) {} 239 | 240 | fn on_update(&self, state: &mut RadioState) { 241 | state.set_disabled(self.state.borrow().disabled()); 242 | } 243 | } 244 | 245 | pub struct MyCheckBoxListener { 246 | state: Rc>, 247 | } 248 | 249 | impl MyCheckBoxListener { 250 | pub fn new(state: Rc>) -> Self { 251 | Self { state } 252 | } 253 | } 254 | 255 | impl CheckBoxListener for MyCheckBoxListener { 256 | fn on_change(&self, _state: &CheckBoxState) {} 257 | 258 | fn on_update(&self, state: &mut CheckBoxState) { 259 | state.set_disabled(self.state.borrow().disabled()); 260 | } 261 | } 262 | 263 | pub struct MyCheckBoxDisabledListener { 264 | state: Rc>, 265 | } 266 | 267 | impl MyCheckBoxDisabledListener { 268 | pub fn new(state: Rc>) -> Self { 269 | Self { state } 270 | } 271 | } 272 | 273 | impl CheckBoxListener for MyCheckBoxDisabledListener { 274 | fn on_change(&self, state: &CheckBoxState) { 275 | self.state.borrow_mut().set_disabled(state.checked()); 276 | } 277 | 278 | fn on_update(&self, state: &mut CheckBoxState) { 279 | state.set_checked(self.state.borrow().disabled()); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /examples/demo_mod/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod listeners; 2 | pub mod models; 3 | -------------------------------------------------------------------------------- /examples/demo_mod/models.rs: -------------------------------------------------------------------------------- 1 | /* current selected tab panel id */ 2 | pub struct Panes { 3 | value: u8, 4 | } 5 | 6 | impl Panes { 7 | pub fn new() -> Self { 8 | Self { value: 0 } 9 | } 10 | 11 | pub fn value(&self) -> u8 { 12 | self.value 13 | } 14 | 15 | pub fn set_value(&mut self, value: u8) { 16 | self.value = value; 17 | } 18 | } 19 | 20 | /* range is the value controlled by the Range and 21 | the TextInput widgets, shown in ProgressBar and a Label. disabled is used 22 | for input widgets */ 23 | pub struct State { 24 | range: i32, 25 | disabled: bool, 26 | } 27 | 28 | impl State { 29 | pub fn new() -> Self { 30 | Self { 31 | range: 0, 32 | disabled: false, 33 | } 34 | } 35 | 36 | pub fn range(&self) -> i32 { 37 | self.range 38 | } 39 | 40 | pub fn set_range(&mut self, range: i32) { 41 | self.range = range; 42 | } 43 | 44 | pub fn disabled(&self) -> bool { 45 | self.disabled 46 | } 47 | 48 | pub fn set_disabled(&mut self, disabled: bool) { 49 | self.disabled = disabled; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use neutrino::widgets::label::Label; 2 | use neutrino::{App, Window}; 3 | 4 | fn main() { 5 | let mut label = Label::new("my_label"); 6 | label.set_text("Hello World !"); 7 | 8 | let mut window = Window::new(); 9 | window.set_title("Hello World"); 10 | window.set_size(320, 240); 11 | window.set_child(Box::new(label)); 12 | window.set_debug(); 13 | 14 | App::run(window); 15 | } 16 | -------------------------------------------------------------------------------- /examples/image_viewer.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use neutrino::utils::icon::BreezeIcon; 5 | use neutrino::widgets::button::Button; 6 | use neutrino::widgets::container::{Container, Direction}; 7 | use neutrino::widgets::image::Image; 8 | use neutrino::widgets::menubar::{MenuBar, MenuFunction, MenuItem}; 9 | use neutrino::utils::event::Key; 10 | use neutrino::{App, Window}; 11 | 12 | mod image_viewer_mod; 13 | use image_viewer_mod::listeners::{ 14 | MyImageListener, MyMenuBarListener, MyNextButtonListener, 15 | MyPrevButtonListener, MyWindowListener, 16 | }; 17 | use image_viewer_mod::models::Images; 18 | 19 | fn main() { 20 | let images = Rc::new(RefCell::new(Images::new())); 21 | 22 | let image_listener = MyImageListener::new(Rc::clone(&images)); 23 | 24 | let mut image = Image::from_path("my_image", ""); 25 | image.set_keep_ratio_aspect(); 26 | image.set_listener(Box::new(image_listener)); 27 | 28 | let prev_listener = MyPrevButtonListener::new(Rc::clone(&images)); 29 | 30 | let mut button_prev = Button::new("button_prev"); 31 | button_prev.set_icon(Box::new(BreezeIcon::Left)); 32 | button_prev.set_listener(Box::new(prev_listener)); 33 | 34 | let next_listener = MyNextButtonListener::new(Rc::clone(&images)); 35 | 36 | let mut button_next = Button::new("button_next"); 37 | button_next.set_icon(Box::new(BreezeIcon::Right)); 38 | button_next.set_listener(Box::new(next_listener)); 39 | 40 | let mut root = Container::new("root"); 41 | root.set_direction(Direction::Horizontal); 42 | root.add(Box::new(button_prev)); 43 | root.add(Box::new(image)); 44 | root.add(Box::new(button_next)); 45 | 46 | let mut prev_function = MenuFunction::new("Previous"); 47 | prev_function.set_shortcut("Ctrl+Left"); 48 | 49 | let mut next_function = MenuFunction::new("Next"); 50 | next_function.set_shortcut("Ctrl+Right"); 51 | 52 | let mut menuitem = MenuItem::new("File", Key::F, 0); 53 | menuitem.add(prev_function); 54 | menuitem.add(next_function); 55 | 56 | let menubar_listener = MyMenuBarListener::new(Rc::clone(&images)); 57 | 58 | let mut menubar = MenuBar::new(); 59 | menubar.add(menuitem); 60 | menubar.set_listener(Box::new(menubar_listener)); 61 | 62 | let window_listener = MyWindowListener::new(Rc::clone(&images)); 63 | 64 | let mut window = Window::new(); 65 | window.set_title("Image viewer"); 66 | window.set_size(640, 480); 67 | window.set_child(Box::new(root)); 68 | window.set_menubar(menubar); 69 | window.set_listener(Box::new(window_listener)); 70 | window.set_debug(); 71 | 72 | App::run(window); 73 | } 74 | -------------------------------------------------------------------------------- /examples/image_viewer_mod/listeners.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashSet; 3 | use std::rc::Rc; 4 | 5 | use neutrino::utils::event::Key; 6 | use neutrino::utils::pixmap::Pixmap; 7 | use neutrino::widgets::button::{ButtonListener, ButtonState}; 8 | use neutrino::widgets::image::{ImageListener, ImageState}; 9 | use neutrino::widgets::menubar::{MenuBarListener, MenuBarState}; 10 | use neutrino::WindowListener; 11 | 12 | use super::models::Images; 13 | 14 | pub struct MyImageListener { 15 | images: Rc>, 16 | } 17 | 18 | impl MyImageListener { 19 | pub fn new(images: Rc>) -> Self { 20 | Self { images } 21 | } 22 | } 23 | 24 | impl ImageListener for MyImageListener { 25 | fn on_update(&self, state: &mut ImageState) { 26 | let pixmap = Pixmap::from_path(self.images.borrow().selected_path()); 27 | state.set_data(pixmap.data()); 28 | state.set_extension(pixmap.extension()); 29 | } 30 | } 31 | 32 | pub struct MyPrevButtonListener { 33 | images: Rc>, 34 | } 35 | 36 | impl MyPrevButtonListener { 37 | pub fn new(images: Rc>) -> Self { 38 | Self { images } 39 | } 40 | } 41 | 42 | impl ButtonListener for MyPrevButtonListener { 43 | fn on_change(&self, _state: &ButtonState) { 44 | self.images.borrow_mut().previous(); 45 | } 46 | 47 | fn on_update(&self, _state: &mut ButtonState) {} 48 | } 49 | 50 | pub struct MyNextButtonListener { 51 | images: Rc>, 52 | } 53 | 54 | impl MyNextButtonListener { 55 | pub fn new(images: Rc>) -> Self { 56 | Self { images } 57 | } 58 | } 59 | 60 | impl ButtonListener for MyNextButtonListener { 61 | fn on_change(&self, _state: &ButtonState) { 62 | self.images.borrow_mut().next(); 63 | } 64 | 65 | fn on_update(&self, _state: &mut ButtonState) {} 66 | } 67 | 68 | pub struct MyMenuBarListener { 69 | images: Rc>, 70 | } 71 | 72 | impl MyMenuBarListener { 73 | pub fn new(images: Rc>) -> Self { 74 | Self { images } 75 | } 76 | } 77 | 78 | impl MenuBarListener for MyMenuBarListener { 79 | fn on_change(&self, state: &MenuBarState) { 80 | match state.selected_item() { 81 | None => (), 82 | Some(_selected_item) => match state.selected_function() { 83 | None => (), 84 | Some(selected_function) => { 85 | if selected_function == 0 { 86 | self.images.borrow_mut().previous(); 87 | } else { 88 | self.images.borrow_mut().next(); 89 | } 90 | } 91 | }, 92 | } 93 | } 94 | } 95 | 96 | pub struct MyWindowListener { 97 | images: Rc>, 98 | } 99 | 100 | impl MyWindowListener { 101 | pub fn new(images: Rc>) -> Self { 102 | Self { images } 103 | } 104 | } 105 | 106 | impl WindowListener for MyWindowListener { 107 | fn on_keys(&self, keys: HashSet) { 108 | if keys.contains(&Key::Control) { 109 | if keys.contains(&Key::Left) { 110 | self.images.borrow_mut().previous(); 111 | } else if keys.contains(&Key::Right) { 112 | self.images.borrow_mut().next(); 113 | } 114 | } 115 | } 116 | 117 | fn on_tick(&self) {} 118 | } 119 | -------------------------------------------------------------------------------- /examples/image_viewer_mod/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod listeners; 2 | pub mod models; 3 | -------------------------------------------------------------------------------- /examples/image_viewer_mod/models.rs: -------------------------------------------------------------------------------- 1 | pub struct Images { 2 | paths: Vec, 3 | selected: usize, 4 | } 5 | 6 | impl Images { 7 | pub fn new() -> Self { 8 | Self { 9 | paths: vec![ 10 | "/home/alexis/Images/autumn.jpg".to_string(), 11 | "/home/alexis/Images/leaf.jpg".to_string(), 12 | "/home/alexis/Images/snow.jpg".to_string(), 13 | ], 14 | selected: 0, 15 | } 16 | } 17 | 18 | pub fn selected_path(&self) -> &str { 19 | &self.paths[self.selected] 20 | } 21 | 22 | pub fn next(&mut self) { 23 | self.selected = if self.selected == 2 { 24 | 0 25 | } else { 26 | self.selected + 1 27 | } 28 | } 29 | 30 | pub fn previous(&mut self) { 31 | self.selected = if self.selected == 0 { 32 | 2 33 | } else { 34 | self.selected - 1 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/login.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use neutrino::widgets::button::{Button, ButtonListener, ButtonState}; 5 | use neutrino::widgets::container::{Container, Direction, Position}; 6 | use neutrino::widgets::label::Label; 7 | use neutrino::widgets::textinput::{ 8 | InputType, TextInput, TextInputListener, TextInputState, 9 | }; 10 | use neutrino::{App, Window}; 11 | 12 | struct Login { 13 | username: String, 14 | password: String, 15 | ok: bool, 16 | } 17 | 18 | impl Login { 19 | fn new() -> Self { 20 | Self { 21 | username: "".to_string(), 22 | password: "".to_string(), 23 | ok: false, 24 | } 25 | } 26 | 27 | fn check(&mut self) { 28 | self.ok = 29 | &self.username == "Neutrino" && &self.password == "is great !"; 30 | } 31 | 32 | fn ok(&self) -> bool { 33 | self.ok 34 | } 35 | 36 | fn set_username(&mut self, username: &str) { 37 | self.username = username.to_string(); 38 | } 39 | 40 | fn set_password(&mut self, password: &str) { 41 | self.password = password.to_string(); 42 | } 43 | } 44 | 45 | struct MyButtonListener { 46 | login: Rc>, 47 | } 48 | 49 | impl MyButtonListener { 50 | fn new(login: Rc>) -> Self { 51 | Self { login } 52 | } 53 | } 54 | 55 | impl ButtonListener for MyButtonListener { 56 | fn on_change(&self, _state: &ButtonState) { 57 | self.login.borrow_mut().check(); 58 | } 59 | 60 | fn on_update(&self, state: &mut ButtonState) { 61 | if self.login.borrow().ok() { 62 | state.set_style( 63 | r#" 64 | $color: forestgreen; 65 | background-color: $color; 66 | border-color: $color; 67 | "#, 68 | ); 69 | state.set_text("OK"); 70 | } else { 71 | state.set_style( 72 | r#" 73 | $color: crimson; 74 | background-color: $color; 75 | border-color: $color; 76 | "#, 77 | ); 78 | state.set_text("KO"); 79 | } 80 | } 81 | } 82 | 83 | struct MyUsernameListener { 84 | login: Rc>, 85 | } 86 | 87 | impl MyUsernameListener { 88 | fn new(login: Rc>) -> Self { 89 | Self { login } 90 | } 91 | } 92 | 93 | impl TextInputListener for MyUsernameListener { 94 | fn on_update(&self, _state: &mut TextInputState) {} 95 | 96 | fn on_change(&self, state: &TextInputState) { 97 | self.login.borrow_mut().set_username(state.value()); 98 | } 99 | } 100 | 101 | struct MyPasswordListener { 102 | login: Rc>, 103 | } 104 | 105 | impl MyPasswordListener { 106 | fn new(login: Rc>) -> Self { 107 | Self { login } 108 | } 109 | } 110 | 111 | impl TextInputListener for MyPasswordListener { 112 | fn on_update(&self, _state: &mut TextInputState) {} 113 | 114 | fn on_change(&self, state: &TextInputState) { 115 | self.login.borrow_mut().set_password(state.value()); 116 | } 117 | } 118 | 119 | fn main() { 120 | let login = Rc::new(RefCell::new(Login::new())); 121 | 122 | let mut username_label = Label::new("username_label"); 123 | username_label.set_text("Username"); 124 | 125 | let mut username_input = TextInput::new("username_input"); 126 | 127 | let username_listener = MyUsernameListener::new(Rc::clone(&login)); 128 | username_input.set_listener(Box::new(username_listener)); 129 | 130 | let mut password_label = Label::new("password_label"); 131 | password_label.set_text("Password"); 132 | 133 | let mut password_input = TextInput::new("password_input"); 134 | password_input.set_input_type(InputType::Password); 135 | 136 | let password_listener = MyPasswordListener::new(Rc::clone(&login)); 137 | password_input.set_listener(Box::new(password_listener)); 138 | 139 | let mut button = Button::new("button"); 140 | button.set_text("Log in"); 141 | 142 | let button_listener = MyButtonListener::new(Rc::clone(&login)); 143 | button.set_listener(Box::new(button_listener)); 144 | 145 | let mut form = Container::new("form"); 146 | form.set_direction(Direction::Vertical); 147 | 148 | form.add(Box::new(username_label)); 149 | form.add(Box::new(username_input)); 150 | form.add(Box::new(password_label)); 151 | form.add(Box::new(password_input)); 152 | 153 | let mut root = Container::new("root"); 154 | root.set_direction(Direction::Vertical); 155 | root.set_position(Position::Between); 156 | 157 | root.add(Box::new(form)); 158 | root.add(Box::new(button)); 159 | 160 | let style = r#" 161 | #app { 162 | background-color: beige; 163 | } 164 | 165 | .textinput { 166 | margin-bottom: 20px; 167 | 168 | input { 169 | width: 100%; 170 | box-sizing: border-box; 171 | border-color: lightgrey; 172 | border-radius: 4px; 173 | 174 | &:focus { 175 | border-color: grey; 176 | } 177 | } 178 | } 179 | 180 | .button { 181 | border-radius: 4px; 182 | color: white; 183 | } 184 | "#; 185 | 186 | let mut window = Window::new(); 187 | window.set_title("Login"); 188 | window.set_size(320, 240); 189 | window.set_child(Box::new(root)); 190 | window.set_style(style); 191 | window.set_debug(); 192 | 193 | App::run(window); 194 | } 195 | -------------------------------------------------------------------------------- /examples/timer.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashSet; 3 | use std::rc::Rc; 4 | 5 | use neutrino::utils::event::Key; 6 | use neutrino::widgets::button::{Button, ButtonListener, ButtonState}; 7 | use neutrino::{App, Window, WindowListener}; 8 | 9 | struct Counter { 10 | value: u8, 11 | } 12 | 13 | impl Counter { 14 | fn new() -> Self { 15 | Self { value: 0 } 16 | } 17 | 18 | fn value(&self) -> u8 { 19 | self.value 20 | } 21 | 22 | fn increment(&mut self) { 23 | self.value += 1; 24 | } 25 | 26 | fn reset(&mut self) { 27 | self.value = 0; 28 | } 29 | } 30 | 31 | struct MyButtonListener { 32 | counter: Rc>, 33 | } 34 | 35 | impl MyButtonListener { 36 | fn new(counter: Rc>) -> Self { 37 | Self { counter } 38 | } 39 | } 40 | 41 | impl ButtonListener for MyButtonListener { 42 | fn on_change(&self, _state: &ButtonState) { 43 | self.counter.borrow_mut().reset(); 44 | } 45 | 46 | fn on_update(&self, state: &mut ButtonState) { 47 | state.set_text(&format!( 48 | "{} seconds since last reset", 49 | self.counter.borrow().value().to_string() 50 | )); 51 | } 52 | } 53 | 54 | struct MyWindowListener { 55 | counter: Rc>, 56 | } 57 | 58 | impl MyWindowListener { 59 | fn new(counter: Rc>) -> Self { 60 | Self { counter } 61 | } 62 | } 63 | 64 | impl WindowListener for MyWindowListener { 65 | fn on_keys(&self, _keys: HashSet) {} 66 | 67 | fn on_tick(&self) { 68 | self.counter.borrow_mut().increment(); 69 | } 70 | } 71 | 72 | fn main() { 73 | let counter = Rc::new(RefCell::new(Counter::new())); 74 | 75 | let listener = MyButtonListener::new(Rc::clone(&counter)); 76 | 77 | let mut button = Button::new("my_button"); 78 | button.set_text("0 seconds since last reset"); 79 | button.set_listener(Box::new(listener)); 80 | 81 | let wlistener = MyWindowListener::new(Rc::clone(&counter)); 82 | 83 | let mut window = Window::new(); 84 | window.set_title("Timer"); 85 | window.set_size(320, 240); 86 | window.set_child(Box::new(button)); 87 | window.set_timer(1000); 88 | window.set_listener(Box::new(wlistener)); 89 | window.set_debug(); 90 | 91 | App::run(window); 92 | } 93 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 -------------------------------------------------------------------------------- /src/utils/event.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | /// # An equivalent of Javascript events 4 | #[derive(Debug)] 5 | pub enum Event { 6 | Undefined, 7 | Update, 8 | Tick, 9 | Change { source: String, value: String }, 10 | Keypress { source: String, keys: HashSet }, 11 | } 12 | 13 | impl Event { 14 | /// Return an one-line function sending a change event from javascript 15 | pub fn change_js(source: &str, value: &str) -> String { 16 | format!( 17 | r#"(function() {{ emit( {{ 18 | type: 'Change', 19 | source: '{}', 20 | value: {} 21 | }} ); event.stopPropagation(); }})()"#, 22 | source, value 23 | ) 24 | } 25 | 26 | /// Return an one-line function sending a key event from javascript 27 | pub fn keypress_js(source: &str, state: &str) -> String { 28 | format!( 29 | r#"(function() {{ emit( {{ 30 | type: 'Keypress', 31 | source: '{}', 32 | state: '{}', 33 | key: event.key 34 | }} ); event.stopPropagation(); }} )()"#, 35 | source, state 36 | ) 37 | } 38 | 39 | /// Return an one-line function setting a timer from javascript 40 | pub fn tick_js(period: u32) -> String { 41 | format!( 42 | r#"setInterval(function() {{ emit( {{ 43 | type: 'Tick' 44 | }} ); }}, {});"#, 45 | period 46 | ) 47 | } 48 | } 49 | 50 | /// # An enum holding a keyboard key 51 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 52 | pub enum Key { 53 | A, 54 | B, 55 | C, 56 | D, 57 | E, 58 | F, 59 | G, 60 | H, 61 | I, 62 | J, 63 | K, 64 | L, 65 | M, 66 | N, 67 | O, 68 | P, 69 | Q, 70 | R, 71 | S, 72 | T, 73 | U, 74 | V, 75 | W, 76 | X, 77 | Y, 78 | Z, 79 | Num0, 80 | Num1, 81 | Num2, 82 | Num3, 83 | Num4, 84 | Num5, 85 | Num6, 86 | Num7, 87 | Num8, 88 | Num9, 89 | Left, 90 | Right, 91 | Up, 92 | Down, 93 | Shift, 94 | Control, 95 | Super, 96 | Alt, 97 | Space, 98 | Escape, 99 | Enter, 100 | } 101 | 102 | impl Key { 103 | /// Return the Key corresponding with the detected keystroke 104 | pub fn new(key: &str) -> Option { 105 | match key { 106 | "a" | "A" => Some(Key::A), 107 | "b" | "B" => Some(Key::B), 108 | "c" | "C" => Some(Key::C), 109 | "d" | "D" => Some(Key::D), 110 | "e" | "E" => Some(Key::E), 111 | "f" | "F" => Some(Key::F), 112 | "g" | "G" => Some(Key::G), 113 | "h" | "H" => Some(Key::H), 114 | "i" | "I" => Some(Key::I), 115 | "j" | "J" => Some(Key::J), 116 | "k" | "K" => Some(Key::K), 117 | "l" | "L" => Some(Key::L), 118 | "m" | "M" => Some(Key::M), 119 | "n" | "N" => Some(Key::N), 120 | "o" | "O" => Some(Key::O), 121 | "p" | "P" => Some(Key::P), 122 | "q" | "Q" => Some(Key::Q), 123 | "r" | "R" => Some(Key::R), 124 | "s" | "S" => Some(Key::S), 125 | "t" | "T" => Some(Key::T), 126 | "u" | "U" => Some(Key::U), 127 | "v" | "V" => Some(Key::V), 128 | "w" | "W" => Some(Key::W), 129 | "x" | "X" => Some(Key::X), 130 | "y" | "Y" => Some(Key::Y), 131 | "z" | "Z" => Some(Key::Z), 132 | "0" => Some(Key::Num0), 133 | "1" => Some(Key::Num1), 134 | "2" => Some(Key::Num2), 135 | "3" => Some(Key::Num3), 136 | "4" => Some(Key::Num4), 137 | "5" => Some(Key::Num5), 138 | "6" => Some(Key::Num6), 139 | "7" => Some(Key::Num7), 140 | "8" => Some(Key::Num8), 141 | "9" => Some(Key::Num9), 142 | "ArrowLeft" => Some(Key::Left), 143 | "ArrowRight" => Some(Key::Right), 144 | "ArrowUp" => Some(Key::Up), 145 | "ArrowDown" => Some(Key::Down), 146 | "Shift" => Some(Key::Space), 147 | "Control" => Some(Key::Control), 148 | "Super" => Some(Key::Super), 149 | "Alt" => Some(Key::Alt), 150 | "Space" => Some(Key::Space), 151 | "Escape" => Some(Key::Escape), 152 | "Enter" => Some(Key::Enter), 153 | _ => None, 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/utils/icon.rs: -------------------------------------------------------------------------------- 1 | /// # Trait that any of the icons have to implement 2 | pub trait Icon { 3 | /// Get the data 4 | fn data(&self) -> String; 5 | 6 | /// Get the extension 7 | fn extension(&self) -> String; 8 | } 9 | 10 | include!(concat!(env!("OUT_DIR"), "/icons/enum.rs")); 11 | 12 | include!(concat!(env!("OUT_DIR"), "/icons/impl.rs")); 13 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod event; 2 | pub mod icon; 3 | pub mod pixmap; 4 | pub mod style; 5 | pub mod theme; 6 | -------------------------------------------------------------------------------- /src/utils/pixmap.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::icon::Icon; 2 | use base64::encode; 3 | use std::fs; 4 | use std::path::Path; 5 | 6 | /// # A model for an image 7 | /// 8 | /// As a webview does not have access to the local file system, the given 9 | /// images are encoded into text (Base64) to be displayed. 10 | /// 11 | /// ## Fields 12 | /// 13 | /// ```text 14 | /// data: String 15 | /// extension: String 16 | /// ``` 17 | pub struct Pixmap { 18 | data: String, 19 | extension: String, 20 | } 21 | 22 | impl Pixmap { 23 | /// Create a Pixmap from text data 24 | pub fn new(data: &str, extension: &str) -> Self { 25 | Pixmap { 26 | data: data.to_string(), 27 | extension: extension.to_string(), 28 | } 29 | } 30 | 31 | /// Create a Pixmap from a file path 32 | pub fn from_path(path: &str) -> Self { 33 | let extension = match Path::new(path).extension() { 34 | Some(ext) => ext.to_str().unwrap().to_string(), 35 | None => "".to_string(), 36 | }; 37 | let data = match fs::read(path) { 38 | Ok(file) => encode(&file), 39 | Err(_) => "".to_string(), 40 | }; 41 | Self { data, extension } 42 | } 43 | 44 | /// Create a Pixmap from an Icon 45 | pub fn from_icon(icon: Box) -> Self { 46 | let extension = icon.extension(); 47 | let data = icon.data(); 48 | Self { data, extension } 49 | } 50 | 51 | /// Get the data 52 | pub fn data(&self) -> &str { 53 | &self.data 54 | } 55 | 56 | /// Get the extension 57 | pub fn extension(&self) -> &str { 58 | match self.extension.as_ref() { 59 | "svg" => "svg+xml", 60 | ext => ext, 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/style.rs: -------------------------------------------------------------------------------- 1 | use rsass::{compile_scss, OutputStyle}; 2 | 3 | /// Transform SCSS into CSS 4 | pub fn scss_to_css(style: &str) -> String { 5 | match compile_scss(style.as_bytes(), OutputStyle::Compressed) { 6 | Ok(css) => match std::str::from_utf8(&css) { 7 | Ok(css) => css.to_string(), 8 | Err(_) => "".to_string(), 9 | }, 10 | Err(_) => "".to_string(), 11 | } 12 | .replace("\n", "") 13 | } 14 | 15 | /// Return the HTML style tag 16 | pub fn inline_style(s: &str) -> String { 17 | format!(r#""#, s) 18 | } 19 | 20 | /// Return the HTML script tag 21 | pub fn inline_script(s: &str) -> String { 22 | format!(r#""#, s) 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/theme.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/themes/enum.rs")); 2 | 3 | include!(concat!(env!("OUT_DIR"), "/themes/impl.rs")); 4 | -------------------------------------------------------------------------------- /src/widgets/button.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::icon::Icon; 3 | use crate::utils::pixmap::Pixmap; 4 | use crate::utils::style::{inline_style, scss_to_css}; 5 | use crate::widgets::widget::Widget; 6 | 7 | /// # The state of a Button 8 | /// 9 | /// ## Fields 10 | /// 11 | /// ```text 12 | /// text: Option 13 | /// icon_data: Option 14 | /// icon_extension: Option 15 | /// disabled: bool 16 | /// stretched: bool 17 | /// style: String 18 | /// ``` 19 | pub struct ButtonState { 20 | text: Option, 21 | icon_data: Option, 22 | icon_extension: Option, 23 | disabled: bool, 24 | stretched: bool, 25 | style: String, 26 | } 27 | 28 | impl ButtonState { 29 | /// Get the text 30 | pub fn text(&self) -> Option<&str> { 31 | self.text.as_ref().map(String::as_ref) 32 | } 33 | 34 | // Get the icon 35 | pub fn icon(&self) -> Option { 36 | match (&self.icon_data, &self.icon_extension) { 37 | (Some(data), Some(extension)) => Some(Pixmap::new(data, extension)), 38 | _ => None, 39 | } 40 | } 41 | 42 | /// Get the disabled flag 43 | pub fn disabled(&self) -> bool { 44 | self.disabled 45 | } 46 | 47 | /// Get the stretched flag 48 | pub fn stretched(&self) -> bool { 49 | self.stretched 50 | } 51 | 52 | /// Get the style 53 | pub fn style(&self) -> &str { 54 | &self.style 55 | } 56 | 57 | /// Set the text 58 | pub fn set_text(&mut self, text: &str) { 59 | self.text = Some(text.to_string()); 60 | } 61 | 62 | /// Set the icon 63 | pub fn set_icon(&mut self, icon: Box) { 64 | let pixmap = Pixmap::from_icon(icon); 65 | self.icon_data = Some(pixmap.data().to_string()); 66 | self.icon_extension = Some(pixmap.extension().to_string()); 67 | } 68 | 69 | /// Set the disabled flag 70 | pub fn set_disabled(&mut self, disabled: bool) { 71 | self.disabled = disabled; 72 | } 73 | 74 | /// Set the streched flag 75 | pub fn set_stretched(&mut self, stretched: bool) { 76 | self.stretched = stretched; 77 | } 78 | 79 | /// Set the style 80 | pub fn set_style(&mut self, style: &str) { 81 | self.style = style.to_string(); 82 | } 83 | } 84 | 85 | /// # The listener of a Button 86 | pub trait ButtonListener { 87 | /// Function triggered on change event 88 | fn on_change(&self, state: &ButtonState); 89 | 90 | /// Function triggered on update event 91 | fn on_update(&self, state: &mut ButtonState); 92 | } 93 | 94 | /// # A clickable button with a label 95 | /// 96 | /// ## Fields 97 | /// 98 | /// ```text 99 | /// name: String 100 | /// state: ButtonState 101 | /// listener: Option> 102 | /// ``` 103 | /// 104 | /// ## Default values 105 | /// 106 | /// ```text 107 | /// name: name.to_string() 108 | /// state: 109 | /// text: None 110 | /// icon_data: None 111 | /// icon_extension: None 112 | /// disabled: false 113 | /// stretched: false 114 | /// style: "".to_string() 115 | /// listener: None 116 | /// ``` 117 | /// 118 | /// ## Style 119 | /// 120 | /// ```text 121 | /// div.button[.disabled] 122 | /// img 123 | /// ``` 124 | /// 125 | /// ## Example 126 | /// 127 | /// ``` 128 | /// use std::cell::RefCell; 129 | /// use std::rc::Rc; 130 | /// 131 | /// use neutrino::widgets::button::{Button, ButtonListener, ButtonState}; 132 | /// use neutrino::utils::theme::Theme; 133 | /// use neutrino::{App, Window}; 134 | /// 135 | /// 136 | /// struct Counter { 137 | /// value: u8, 138 | /// } 139 | /// 140 | /// impl Counter { 141 | /// fn new() -> Self { 142 | /// Self { value: 0 } 143 | /// } 144 | /// 145 | /// fn value(&self) -> u8 { 146 | /// self.value 147 | /// } 148 | /// 149 | /// fn increment(&mut self) { 150 | /// self.value += 1; 151 | /// } 152 | /// } 153 | /// 154 | /// 155 | /// struct MyButtonListener { 156 | /// counter: Rc>, 157 | /// } 158 | /// 159 | /// impl MyButtonListener { 160 | /// pub fn new(counter: Rc>) -> Self { 161 | /// Self { counter } 162 | /// } 163 | /// } 164 | /// 165 | /// impl ButtonListener for MyButtonListener { 166 | /// fn on_change(&self, _state: &ButtonState) { 167 | /// self.counter.borrow_mut().increment(); 168 | /// } 169 | /// 170 | /// fn on_update(&self, state: &mut ButtonState) { 171 | /// state.set_text(&self.counter.borrow().value().to_string()); 172 | /// } 173 | /// } 174 | /// 175 | /// 176 | /// fn main() { 177 | /// let counter = Rc::new(RefCell::new(Counter::new())); 178 | /// 179 | /// let my_listener = MyButtonListener::new(Rc::clone(&counter)); 180 | /// 181 | /// let mut my_button = Button::new("my_button"); 182 | /// my_button.set_text("Click me !"); 183 | /// my_button.set_listener(Box::new(my_listener)); 184 | /// } 185 | /// ``` 186 | pub struct Button { 187 | name: String, 188 | state: ButtonState, 189 | listener: Option>, 190 | } 191 | 192 | impl Button { 193 | /// Create a Button 194 | pub fn new(name: &str) -> Self { 195 | Self { 196 | name: name.to_string(), 197 | state: ButtonState { 198 | text: None, 199 | icon_data: None, 200 | icon_extension: None, 201 | disabled: false, 202 | stretched: false, 203 | style: "".to_string(), 204 | }, 205 | listener: None, 206 | } 207 | } 208 | 209 | /// Set the text 210 | pub fn set_text(&mut self, text: &str) { 211 | self.state.set_text(text); 212 | } 213 | 214 | /// Set the icon 215 | pub fn set_icon(&mut self, icon: Box) { 216 | self.state.set_icon(icon); 217 | } 218 | 219 | /// Set the disabled flag to true 220 | pub fn set_disabled(&mut self) { 221 | self.state.set_disabled(true); 222 | } 223 | 224 | /// Set the stretched flag to true 225 | pub fn set_stretched(&mut self) { 226 | self.state.set_stretched(true); 227 | } 228 | 229 | /// Set the listener 230 | pub fn set_listener(&mut self, listener: Box) { 231 | self.listener = Some(listener); 232 | } 233 | 234 | /// Set the style 235 | pub fn set_style(&mut self, style: &str) { 236 | self.state.set_style(style); 237 | } 238 | } 239 | 240 | impl Widget for Button { 241 | fn eval(&self) -> String { 242 | let disabled = if self.state.disabled() { 243 | "disabled" 244 | } else { 245 | "" 246 | }; 247 | let stretched = if self.state.stretched() { 248 | "stretched" 249 | } else { 250 | "" 251 | }; 252 | let style = inline_style(&scss_to_css(&format!( 253 | r##"#{}{{{}}}"##, 254 | self.name, 255 | self.state.style(), 256 | ))); 257 | let html = match (self.state.text(), self.state.icon()) { 258 | (Some(text), Some(icon)) => format!( 259 | r#" 260 |
261 | 262 | {} 263 |
264 | "#, 265 | self.name, 266 | disabled, 267 | stretched, 268 | Event::change_js(&self.name, "''"), 269 | icon.extension(), 270 | icon.data(), 271 | text, 272 | ), 273 | (Some(text), None) => format!( 274 | r#" 275 |
276 | {} 277 |
278 | "#, 279 | self.name, 280 | disabled, 281 | stretched, 282 | Event::change_js(&self.name, "''"), 283 | text, 284 | ), 285 | (None, Some(icon)) => format!( 286 | r#" 287 |
288 | 289 |
290 | "#, 291 | self.name, 292 | disabled, 293 | stretched, 294 | Event::change_js(&self.name, "''"), 295 | icon.extension(), 296 | icon.data(), 297 | ), 298 | (None, None) => format!( 299 | r#" 300 |
301 | {} 302 |
303 | "#, 304 | self.name, 305 | disabled, 306 | stretched, 307 | Event::change_js(&self.name, "''"), 308 | "No text", 309 | ), 310 | }; 311 | format!("{}{}", style, html) 312 | } 313 | 314 | fn trigger(&mut self, event: &Event) { 315 | match event { 316 | Event::Update => self.on_update(), 317 | Event::Change { source, value } => { 318 | if source == &self.name && !self.state.disabled() { 319 | self.on_change(value) 320 | } 321 | } 322 | _ => (), 323 | } 324 | } 325 | 326 | fn on_update(&mut self) { 327 | match &self.listener { 328 | None => (), 329 | Some(listener) => { 330 | listener.on_update(&mut self.state); 331 | } 332 | } 333 | } 334 | 335 | fn on_change(&mut self, _value: &str) { 336 | match &self.listener { 337 | None => (), 338 | Some(listener) => { 339 | listener.on_change(&self.state); 340 | } 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/widgets/checkbox.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::style::{inline_style, scss_to_css}; 3 | use crate::widgets::widget::Widget; 4 | 5 | /// # The state of a CheckBox 6 | /// 7 | /// ## Fields 8 | /// 9 | /// ```text 10 | /// text: String 11 | /// checked: bool 12 | /// disabled: bool 13 | /// stretched: bool 14 | /// style: String 15 | /// ``` 16 | pub struct CheckBoxState { 17 | text: String, 18 | checked: bool, 19 | disabled: bool, 20 | stretched: bool, 21 | style: String, 22 | } 23 | 24 | impl CheckBoxState { 25 | /// Get the text 26 | pub fn text(&self) -> &str { 27 | &self.text 28 | } 29 | 30 | /// Get the checked flag 31 | pub fn checked(&self) -> bool { 32 | self.checked 33 | } 34 | 35 | /// Get the disabled flag 36 | pub fn disabled(&self) -> bool { 37 | self.disabled 38 | } 39 | 40 | /// Get the stretched flag 41 | pub fn stretched(&self) -> bool { 42 | self.stretched 43 | } 44 | 45 | /// Get the style 46 | pub fn style(&self) -> &str { 47 | &self.style 48 | } 49 | 50 | /// Set the text 51 | pub fn set_text(&mut self, text: &str) { 52 | self.text = text.to_string(); 53 | } 54 | 55 | /// Set the checked flag 56 | pub fn set_checked(&mut self, checked: bool) { 57 | self.checked = checked; 58 | } 59 | 60 | /// Set the disabled flag 61 | pub fn set_disabled(&mut self, disabled: bool) { 62 | self.disabled = disabled; 63 | } 64 | 65 | /// Set the streched flag 66 | pub fn set_stretched(&mut self, stretched: bool) { 67 | self.stretched = stretched; 68 | } 69 | 70 | /// Set the style 71 | pub fn set_style(&mut self, style: &str) { 72 | self.style = style.to_string(); 73 | } 74 | } 75 | 76 | /// # The listener of a Checkbox 77 | pub trait CheckBoxListener { 78 | /// Function triggered on change event 79 | fn on_change(&self, state: &CheckBoxState); 80 | 81 | /// Function triggered on update event 82 | fn on_update(&self, state: &mut CheckBoxState); 83 | } 84 | 85 | /// # A togglable checkbox with a label 86 | /// 87 | /// ## Fields 88 | /// 89 | /// ```text 90 | /// name: String 91 | /// state: CheckBoxState 92 | /// listener: Option> 93 | /// ``` 94 | /// 95 | /// ## Default values 96 | /// 97 | /// ```text 98 | /// name: name.to_string() 99 | /// state: 100 | /// text: "CheckBox".to_string() 101 | /// checked: false 102 | /// disabled: false 103 | /// stretched: false 104 | /// style: "".to_string() 105 | /// listener: None 106 | /// ``` 107 | /// 108 | /// ## Style 109 | /// 110 | /// ```text 111 | /// div.checkbox[.disabled][.checked] 112 | /// div.checkbox-outer 113 | /// div.checkbox-inner 114 | /// label 115 | /// ``` 116 | /// 117 | /// ## Example 118 | /// 119 | /// ``` 120 | /// use std::cell::RefCell; 121 | /// use std::rc::Rc; 122 | /// 123 | /// use neutrino::widgets::checkbox::{ 124 | /// CheckBox, 125 | /// CheckBoxListener, 126 | /// CheckBoxState 127 | /// }; 128 | /// use neutrino::utils::theme::Theme; 129 | /// use neutrino::{App, Window}; 130 | /// 131 | /// 132 | /// struct Switch { 133 | /// value: bool, 134 | /// } 135 | /// 136 | /// impl Switch { 137 | /// fn new() -> Self { 138 | /// Self { value: false } 139 | /// } 140 | /// 141 | /// fn value(&self) -> bool { 142 | /// self.value 143 | /// } 144 | /// 145 | /// fn toggle(&mut self) { 146 | /// self.value = !self.value; 147 | /// } 148 | /// } 149 | /// 150 | /// 151 | /// struct MyCheckBoxListener { 152 | /// switch: Rc>, 153 | /// } 154 | /// 155 | /// impl MyCheckBoxListener { 156 | /// pub fn new(switch: Rc>) -> Self { 157 | /// Self { switch } 158 | /// } 159 | /// } 160 | /// 161 | /// impl CheckBoxListener for MyCheckBoxListener { 162 | /// fn on_change(&self, _state: &CheckBoxState) { 163 | /// self.switch.borrow_mut().toggle(); 164 | /// } 165 | /// 166 | /// fn on_update(&self, state: &mut CheckBoxState) { 167 | /// state.set_checked(self.switch.borrow().value()); 168 | /// } 169 | /// } 170 | /// 171 | /// 172 | /// fn main() { 173 | /// let switch = Rc::new(RefCell::new(Switch::new())); 174 | /// 175 | /// let my_listener = MyCheckBoxListener::new(Rc::clone(&switch)); 176 | /// 177 | /// let mut my_checkbox = CheckBox::new("my_checkbox"); 178 | /// my_checkbox.set_text("Toggle me !"); 179 | /// my_checkbox.set_listener(Box::new(my_listener)); 180 | /// } 181 | /// ``` 182 | pub struct CheckBox { 183 | name: String, 184 | state: CheckBoxState, 185 | listener: Option>, 186 | } 187 | 188 | impl CheckBox { 189 | /// Create a CheckBox 190 | pub fn new(name: &str) -> Self { 191 | Self { 192 | name: name.to_string(), 193 | state: CheckBoxState { 194 | text: "CheckBox".to_string(), 195 | checked: false, 196 | disabled: false, 197 | stretched: false, 198 | style: "".to_string(), 199 | }, 200 | listener: None, 201 | } 202 | } 203 | 204 | /// Set the text 205 | pub fn set_text(&mut self, text: &str) { 206 | self.state.set_text(text); 207 | } 208 | 209 | /// Set the checked flag to true 210 | pub fn set_checked(&mut self) { 211 | self.state.set_checked(true); 212 | } 213 | 214 | /// Set the disabled flag to true 215 | pub fn set_disabled(&mut self) { 216 | self.state.set_disabled(true); 217 | } 218 | 219 | /// Set the stretched flag to true 220 | pub fn set_stretched(&mut self) { 221 | self.state.set_stretched(true); 222 | } 223 | 224 | /// Set the listener 225 | pub fn set_listener(&mut self, listener: Box) { 226 | self.listener = Some(listener); 227 | } 228 | 229 | /// Set the style 230 | pub fn set_style(&mut self, style: &str) { 231 | self.state.set_style(style); 232 | } 233 | } 234 | 235 | impl Widget for CheckBox { 236 | fn eval(&self) -> String { 237 | let checked = if self.state.checked() { "checked" } else { "" }; 238 | let stretched = if self.state.stretched() { 239 | "stretched" 240 | } else { 241 | "" 242 | }; 243 | let disabled = if self.state.disabled() { 244 | "disabled" 245 | } else { 246 | "" 247 | }; 248 | let style = inline_style(&scss_to_css(&format!( 249 | r##"#{}{{{}}}"##, 250 | self.name, 251 | self.state.style(), 252 | ))); 253 | let html = format!( 254 | r#" 255 |
256 |
257 |
258 |
259 |
260 | 261 |
262 | "#, 263 | self.name, 264 | disabled, 265 | checked, 266 | stretched, 267 | Event::change_js(&self.name, "''"), 268 | self.state.text, 269 | ); 270 | format!("{}{}", style, html) 271 | } 272 | 273 | fn trigger(&mut self, event: &Event) { 274 | match event { 275 | Event::Update => self.on_update(), 276 | Event::Change { source, value } => { 277 | if source == &self.name && !self.state.disabled() { 278 | self.on_change(value) 279 | } 280 | } 281 | _ => (), 282 | } 283 | } 284 | 285 | fn on_update(&mut self) { 286 | match &self.listener { 287 | None => (), 288 | Some(listener) => { 289 | listener.on_update(&mut self.state); 290 | } 291 | } 292 | } 293 | 294 | fn on_change(&mut self, _value: &str) { 295 | self.state.set_checked(!self.state.checked()); 296 | match &self.listener { 297 | None => (), 298 | Some(listener) => { 299 | listener.on_change(&self.state); 300 | } 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/widgets/combo.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::style::{inline_style, scss_to_css}; 3 | use crate::widgets::widget::Widget; 4 | 5 | /// # The state of a Combo 6 | /// 7 | /// ## Fields 8 | /// 9 | /// ```text 10 | /// choices: Vec 11 | /// selected: u32 12 | /// opened: bool 13 | /// disabled: bool 14 | /// stretched: bool 15 | /// style: String 16 | /// ``` 17 | pub struct ComboState { 18 | choices: Vec, 19 | selected: u32, 20 | opened: bool, 21 | disabled: bool, 22 | stretched: bool, 23 | style: String, 24 | } 25 | 26 | impl ComboState { 27 | /// Get the choices 28 | pub fn choices(&self) -> &Vec { 29 | &self.choices 30 | } 31 | 32 | /// Get the selected flag 33 | pub fn selected(&self) -> u32 { 34 | self.selected 35 | } 36 | 37 | /// Get the opened flag 38 | pub fn opened(&self) -> bool { 39 | self.opened 40 | } 41 | 42 | /// Get the disabled flag 43 | pub fn disabled(&self) -> bool { 44 | self.disabled 45 | } 46 | 47 | /// Get the stretched flag 48 | pub fn stretched(&self) -> bool { 49 | self.stretched 50 | } 51 | 52 | /// Get the style 53 | pub fn style(&self) -> &str { 54 | &self.style 55 | } 56 | 57 | /// Set the choices 58 | pub fn set_choices(&mut self, choices: Vec<&str>) { 59 | self.choices = choices 60 | .iter() 61 | .map(|c| c.to_string()) 62 | .collect::>(); 63 | } 64 | 65 | /// Set the selected flag 66 | pub fn set_selected(&mut self, selected: u32) { 67 | self.selected = selected; 68 | } 69 | 70 | /// Set the opened flag 71 | pub fn set_opened(&mut self, opened: bool) { 72 | self.opened = opened; 73 | } 74 | 75 | /// Set the disabled flag 76 | pub fn set_disabled(&mut self, disabled: bool) { 77 | self.disabled = disabled; 78 | } 79 | 80 | /// Set the stretched flag 81 | pub fn set_stretched(&mut self, stretched: bool) { 82 | self.stretched = stretched; 83 | } 84 | 85 | /// Set the style 86 | pub fn set_style(&mut self, style: &str) { 87 | self.style = style.to_string(); 88 | } 89 | } 90 | 91 | /// # The listener of a Combo 92 | pub trait ComboListener { 93 | /// Function triggered on change event 94 | fn on_change(&self, state: &ComboState); 95 | 96 | /// Function triggered on update event 97 | fn on_update(&self, state: &mut ComboState); 98 | } 99 | 100 | /// # A collapsible list of strings 101 | /// 102 | /// ## Fields 103 | /// 104 | /// ```text 105 | /// name: String 106 | /// state: ComboState 107 | /// listener: Option> 108 | /// ``` 109 | /// 110 | /// ## Default values 111 | /// 112 | /// ```text 113 | /// name: name.to_string() 114 | /// state: 115 | /// choices: vec!["Choice 1".to_string(), "Choice 2".to_string()], 116 | /// selected: 0, 117 | /// opened: false, 118 | /// disabled: false, 119 | /// stretched: false, 120 | /// style: "".to_string() 121 | /// listener: None 122 | /// ``` 123 | /// 124 | /// ## Style 125 | /// 126 | /// ```text 127 | /// div.combo[.opened][.disabled] 128 | /// div.combo-button 129 | /// div.combo-icon 130 | /// div.combo-choices 131 | /// div.combo-choice 132 | /// ``` 133 | /// 134 | /// ## Example 135 | /// 136 | /// ``` 137 | /// use std::cell::RefCell; 138 | /// use std::rc::Rc; 139 | /// 140 | /// use neutrino::widgets::combo::{Combo, ComboListener, ComboState}; 141 | /// use neutrino::utils::theme::Theme; 142 | /// use neutrino::{App, Window}; 143 | /// 144 | /// 145 | /// struct Dessert { 146 | /// index: u32, 147 | /// value: String, 148 | /// } 149 | /// 150 | /// impl Dessert { 151 | /// fn new() -> Self { 152 | /// Self { index: 0, value: "Cake".to_string() } 153 | /// } 154 | /// 155 | /// fn index(&self) -> u32 { 156 | /// self.index 157 | /// } 158 | /// 159 | /// fn value(&self) -> &str { 160 | /// &self.value 161 | /// } 162 | /// 163 | /// fn set(&mut self, index: u32, value: &str) { 164 | /// self.index = index; 165 | /// self.value = value.to_string(); 166 | /// } 167 | /// } 168 | /// 169 | /// 170 | /// struct MyComboListener { 171 | /// dessert: Rc>, 172 | /// } 173 | /// 174 | /// impl MyComboListener { 175 | /// pub fn new(dessert: Rc>) -> Self { 176 | /// Self { dessert } 177 | /// } 178 | /// } 179 | /// 180 | /// impl ComboListener for MyComboListener { 181 | /// fn on_change(&self, state: &ComboState) { 182 | /// let index = state.selected(); 183 | /// self.dessert.borrow_mut().set( 184 | /// index, 185 | /// &state.choices()[index as usize] 186 | /// ); 187 | /// } 188 | /// 189 | /// fn on_update(&self, state: &mut ComboState) { 190 | /// state.set_selected(self.dessert.borrow().index()); 191 | /// } 192 | /// } 193 | /// 194 | /// 195 | /// fn main() { 196 | /// let dessert = Rc::new(RefCell::new(Dessert::new())); 197 | /// 198 | /// let my_listener = MyComboListener::new(Rc::clone(&dessert)); 199 | /// 200 | /// let mut my_combo = Combo::new("my_combo"); 201 | /// my_combo.set_choices(vec!["Cake", "Ice Cream", "Pie"]); 202 | /// my_combo.set_listener(Box::new(my_listener)); 203 | /// } 204 | /// ``` 205 | pub struct Combo { 206 | name: String, 207 | state: ComboState, 208 | listener: Option>, 209 | } 210 | 211 | impl Combo { 212 | /// Create a Combo 213 | pub fn new(name: &str) -> Self { 214 | Self { 215 | name: name.to_string(), 216 | state: ComboState { 217 | choices: vec!["Choice 1".to_string(), "Choice 2".to_string()], 218 | selected: 0, 219 | opened: false, 220 | disabled: false, 221 | stretched: false, 222 | style: "".to_string(), 223 | }, 224 | listener: None, 225 | } 226 | } 227 | 228 | /// Set the choices 229 | pub fn set_choices(&mut self, choices: Vec<&str>) { 230 | self.state.set_choices(choices); 231 | } 232 | 233 | /// Set the index of the selected choice 234 | pub fn set_selected(&mut self, selected: u32) { 235 | self.state.set_selected(selected); 236 | } 237 | 238 | /// Set the opened flag to true 239 | pub fn set_opened(&mut self) { 240 | self.state.set_opened(true); 241 | } 242 | 243 | /// Set the disabled flag to true 244 | pub fn set_disabled(&mut self) { 245 | self.state.set_disabled(true); 246 | } 247 | 248 | /// Set the stretched flag to true 249 | pub fn set_stretched(&mut self) { 250 | self.state.set_stretched(true); 251 | } 252 | 253 | /// Set the listener 254 | pub fn set_listener(&mut self, listener: Box) { 255 | self.listener = Some(listener); 256 | } 257 | 258 | /// Set the style 259 | pub fn set_style(&mut self, style: &str) { 260 | self.state.set_style(style); 261 | } 262 | } 263 | 264 | impl Widget for Combo { 265 | fn eval(&self) -> String { 266 | let stretched = if self.state.stretched() { 267 | "stretched" 268 | } else { 269 | "" 270 | }; 271 | let disabled = if self.state.disabled() { 272 | "disabled" 273 | } else { 274 | "" 275 | }; 276 | let opened = if self.state.opened() { "opened" } else { "" }; 277 | let style = inline_style(&scss_to_css(&format!( 278 | r##"#{}{{{}}}"##, 279 | self.name, 280 | self.state.style(), 281 | ))); 282 | let mut html = format!( 283 | r#" 284 |
285 |
286 | {} 287 |
288 |
289 | "#, 290 | self.name, 291 | stretched, 292 | opened, 293 | disabled, 294 | Event::change_js(&self.name, "'-1'"), 295 | self.state.choices()[self.state.selected() as usize], 296 | ); 297 | if self.state.opened() { 298 | html.push_str(r#"
"#); 299 | let combos_length = self.state.choices().len(); 300 | for (i, choice) in self.state.choices().iter().enumerate() { 301 | let last = if i == combos_length - 1 { "last" } else { "" }; 302 | html.push_str(&format!( 303 | r#" 304 |
305 | {} 306 |
307 | "#, 308 | last, 309 | Event::change_js(&self.name, &format!("'{}'", i)), 310 | choice 311 | )); 312 | } 313 | html.push_str(r#"
"#); 314 | } 315 | html.push_str("
"); 316 | format!("{}{}", style, html) 317 | } 318 | 319 | fn trigger(&mut self, event: &Event) { 320 | match event { 321 | Event::Update => self.on_update(), 322 | Event::Change { source, value } => { 323 | if source == &self.name && !self.state.disabled() { 324 | self.on_change(value); 325 | } else { 326 | self.state.set_opened(false); 327 | } 328 | } 329 | _ => self.state.set_opened(false), 330 | } 331 | } 332 | 333 | fn on_update(&mut self) { 334 | match &self.listener { 335 | None => (), 336 | Some(listener) => { 337 | listener.on_update(&mut self.state); 338 | } 339 | } 340 | } 341 | 342 | fn on_change(&mut self, value: &str) { 343 | self.state.set_opened(!self.state.opened()); 344 | let selected = value.parse::().unwrap(); 345 | if selected > -1 { 346 | self.state.set_selected(selected as u32); 347 | } 348 | match &self.listener { 349 | None => (), 350 | Some(listener) => { 351 | listener.on_change(&self.state); 352 | } 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/widgets/container.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::style::{inline_style, scss_to_css}; 3 | use crate::widgets::widget::Widget; 4 | 5 | /// # The state of a Container 6 | /// 7 | /// ## Fields 8 | /// 9 | /// ```text 10 | /// children: Vec> 11 | /// direction: Direction 12 | /// position: Position 13 | /// alignment: Alignment 14 | /// style: String 15 | /// ``` 16 | pub struct ContainerState { 17 | children: Vec>, 18 | direction: Direction, 19 | position: Position, 20 | alignment: Alignment, 21 | stretched: bool, 22 | style: String, 23 | } 24 | 25 | impl ContainerState { 26 | /// Get the children 27 | pub fn children(&self) -> &Vec> { 28 | &self.children 29 | } 30 | 31 | /// Get the direction 32 | pub fn direction(&self) -> &Direction { 33 | &self.direction 34 | } 35 | 36 | /// Get the position 37 | pub fn position(&self) -> &Position { 38 | &self.position 39 | } 40 | 41 | /// Get the alignment 42 | pub fn alignment(&self) -> &Alignment { 43 | &self.alignment 44 | } 45 | 46 | /// Get the stretched flag 47 | pub fn stretched(&self) -> bool { 48 | self.stretched 49 | } 50 | 51 | /// Get the style 52 | pub fn style(&self) -> &str { 53 | &self.style 54 | } 55 | 56 | /// Set the children 57 | pub fn set_children(&mut self, children: Vec>) { 58 | self.children = children; 59 | } 60 | 61 | /// Set the direction 62 | pub fn set_direction(&mut self, direction: Direction) { 63 | self.direction = direction; 64 | } 65 | 66 | /// Set the position 67 | pub fn set_position(&mut self, position: Position) { 68 | self.position = position; 69 | } 70 | 71 | /// Set the alignment 72 | pub fn set_alignment(&mut self, alignment: Alignment) { 73 | self.alignment = alignment; 74 | } 75 | 76 | /// Set the stretched flag 77 | pub fn set_stretched(&mut self, stretched: bool) { 78 | self.stretched = stretched; 79 | } 80 | 81 | /// Set the style 82 | pub fn set_style(&mut self, style: &str) { 83 | self.style = style.to_string(); 84 | } 85 | 86 | /// Add a child 87 | fn add(&mut self, child: Box) { 88 | self.children.push(child); 89 | } 90 | } 91 | 92 | /// # The listener of a Container 93 | pub trait ContainerListener { 94 | /// Function triggered on update event 95 | fn on_update(&self, state: &mut ContainerState); 96 | } 97 | 98 | /// # A container for other widgets 99 | /// 100 | /// ## Fields 101 | /// 102 | /// ```text 103 | /// name: String 104 | /// state: ContainerState 105 | /// listener: Option> 106 | /// ``` 107 | /// 108 | /// ## Default values 109 | /// 110 | /// ```text 111 | /// name: name.to_string() 112 | /// state: 113 | /// children: vec![] 114 | /// direction: Direction::Vertical 115 | /// position: Position::Start 116 | /// alignment: Alignment::None 117 | /// stretched: false 118 | /// style: "".to_string() 119 | /// listener: None 120 | /// ``` 121 | /// 122 | /// ## Style 123 | /// 124 | /// ```text 125 | /// div.container 126 | /// ``` 127 | /// 128 | /// ## Example 129 | /// 130 | /// ``` 131 | /// use std::cell::RefCell; 132 | /// use std::rc::Rc; 133 | /// 134 | /// use neutrino::widgets::container::{ 135 | /// Container, 136 | /// ContainerListener, 137 | /// ContainerState 138 | /// }; 139 | /// use neutrino::widgets::label::Label; 140 | /// use neutrino::widgets::widget::Widget; 141 | /// use neutrino::utils::theme::Theme; 142 | /// use neutrino::{App, Window}; 143 | /// 144 | /// 145 | /// struct Quotes { 146 | /// values: Vec, 147 | /// } 148 | /// 149 | /// impl Quotes { 150 | /// fn new() -> Self { 151 | /// Self { values: vec![] } 152 | /// } 153 | /// 154 | /// fn values(&self) -> &Vec { 155 | /// &self.values 156 | /// } 157 | /// } 158 | /// 159 | /// 160 | /// struct MyContainerListener { 161 | /// quotes: Rc>, 162 | /// } 163 | /// 164 | /// impl MyContainerListener { 165 | /// pub fn new(quotes: Rc>) -> Self { 166 | /// Self { quotes } 167 | /// } 168 | /// } 169 | /// 170 | /// impl ContainerListener for MyContainerListener { 171 | /// fn on_update(&self, state: &mut ContainerState) { 172 | /// let labels = self.quotes.borrow().values().iter().enumerate().map( 173 | /// |(i, q)| { 174 | /// let name = format!("quote-{}", i); 175 | /// let mut w = Label::new(&name); 176 | /// w.set_text(q); 177 | /// let b: Box = Box::new(w); 178 | /// b 179 | /// }).collect::>>(); 180 | /// state.set_children(labels); 181 | /// } 182 | /// } 183 | /// 184 | /// 185 | /// fn main() { 186 | /// let quotes = Rc::new(RefCell::new(Quotes::new())); 187 | /// 188 | /// let my_listener = MyContainerListener::new(Rc::clone("es)); 189 | /// 190 | /// let mut my_container = Container::new("my_container"); 191 | /// my_container.set_listener(Box::new(my_listener)); 192 | /// } 193 | /// ``` 194 | pub struct Container { 195 | name: String, 196 | state: ContainerState, 197 | listener: Option>, 198 | } 199 | 200 | impl Container { 201 | /// Create a Container 202 | pub fn new(name: &str) -> Self { 203 | Self { 204 | name: name.to_string(), 205 | state: ContainerState { 206 | children: vec![], 207 | direction: Direction::Vertical, 208 | position: Position::Start, 209 | alignment: Alignment::None, 210 | stretched: false, 211 | style: "".to_string(), 212 | }, 213 | listener: None, 214 | } 215 | } 216 | 217 | /// Set the direction 218 | pub fn set_direction(&mut self, direction: Direction) { 219 | self.state.set_direction(direction); 220 | } 221 | 222 | /// Set the position 223 | pub fn set_position(&mut self, position: Position) { 224 | self.state.set_position(position); 225 | } 226 | 227 | /// Set the alignment 228 | pub fn set_alignment(&mut self, alignment: Alignment) { 229 | self.state.set_alignment(alignment); 230 | } 231 | 232 | /// Set the stretched flag to true. Alignment needs to be set to 233 | /// Alignment::None (default) for the Container to stretch. 234 | pub fn set_stretched(&mut self) { 235 | self.state.set_stretched(true); 236 | } 237 | 238 | /// Set the listener 239 | pub fn set_listener(&mut self, listener: Box) { 240 | self.listener.replace(listener); 241 | } 242 | 243 | /// Set the style 244 | pub fn set_style(&mut self, style: &str) { 245 | self.state.set_style(style); 246 | } 247 | 248 | /// Add a widget 249 | pub fn add(&mut self, widget: Box) { 250 | self.state.add(widget); 251 | } 252 | } 253 | 254 | impl Widget for Container { 255 | fn eval(&self) -> String { 256 | let stretched = if self.state.stretched() { 257 | "stretched" 258 | } else { 259 | "" 260 | }; 261 | let style = inline_style(&scss_to_css(&format!( 262 | r##"#{}{{{}}}"##, 263 | self.name, 264 | self.state.style(), 265 | ))); 266 | let mut html = format!( 267 | r#"
"#, 268 | self.name, 269 | self.state.position().css(), 270 | self.state.direction().css(), 271 | self.state.alignment().css(), 272 | stretched, 273 | ); 274 | for widget in self.state.children.iter() { 275 | html.push_str(&widget.eval()); 276 | } 277 | html.push_str("
"); 278 | format!("{}{}", style, html) 279 | } 280 | 281 | fn trigger(&mut self, event: &Event) { 282 | match event { 283 | Event::Update => self.on_update(), 284 | Event::Change { source, value } => { 285 | if source == &self.name { 286 | self.on_change(value) 287 | } 288 | } 289 | _ => (), 290 | } 291 | for widget in self.state.children.iter_mut() { 292 | widget.trigger(event); 293 | } 294 | } 295 | 296 | fn on_update(&mut self) { 297 | match &self.listener { 298 | None => (), 299 | Some(listener) => { 300 | listener.on_update(&mut self.state); 301 | } 302 | } 303 | } 304 | 305 | fn on_change(&mut self, _value: &str) {} 306 | } 307 | 308 | /// # The direction of a Container 309 | pub enum Direction { 310 | Horizontal, 311 | Vertical, 312 | } 313 | 314 | impl Direction { 315 | // Return the CSS class corresponding to the direction 316 | pub fn css(&self) -> &str { 317 | match &self { 318 | Direction::Horizontal => "direction-horizontal", 319 | Direction::Vertical => "direction-vertical", 320 | } 321 | } 322 | } 323 | 324 | /// # The position of the elements inside of a Container 325 | /// 326 | /// The position is defined on the direction axis. 327 | /// 328 | /// ## Example 329 | /// 330 | /// ```text 331 | /// Direction::Horizontal 332 | /// Position::Start 333 | /// 334 | /// +-----------------------------+ 335 | /// | +--------+ +--------+ | 336 | /// | | widget | | widget | | 337 | /// | +--------+ +--------+ | 338 | /// +-----------------------------+ 339 | /// ``` 340 | pub enum Position { 341 | Center, 342 | Start, 343 | End, 344 | Between, 345 | Around, 346 | } 347 | 348 | impl Position { 349 | // Return the CSS class corresponding to the position 350 | fn css(&self) -> &str { 351 | match &self { 352 | Position::Center => "position-center", 353 | Position::Start => "position-start", 354 | Position::End => "position-end", 355 | Position::Between => "position-between", 356 | Position::Around => "position-around", 357 | } 358 | } 359 | } 360 | 361 | /// # The alignment of a Container 362 | /// 363 | /// The alignment is defined on the perpendicular axis of the direction axis. 364 | /// 365 | /// ## Example 366 | /// 367 | /// ```text 368 | /// Direction::Vertical 369 | /// Alignement::Center 370 | /// 371 | /// +----------------------+ 372 | /// | +--------+ | 373 | /// | | widget | | 374 | /// | +--------+ | 375 | /// | +--------+ | 376 | /// | | widget | | 377 | /// | +--------+ | 378 | /// +----------------------+ 379 | /// ``` 380 | pub enum Alignment { 381 | None, 382 | Center, 383 | Start, 384 | End, 385 | } 386 | 387 | impl Alignment { 388 | // Return the CSS class corresponding to the alignment 389 | fn css(&self) -> &str { 390 | match &self { 391 | Alignment::None => "", 392 | Alignment::Center => "alignment-center", 393 | Alignment::Start => "alignment-start", 394 | Alignment::End => "alignment-end", 395 | } 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/widgets/image.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::icon::Icon; 3 | use crate::utils::pixmap::Pixmap; 4 | use crate::utils::style::{inline_style, scss_to_css}; 5 | use crate::widgets::widget::Widget; 6 | 7 | /// # The state of an Image 8 | /// 9 | /// ## Fields 10 | /// 11 | /// ```text 12 | /// data: String 13 | /// extension: String 14 | /// background: String 15 | /// keep_ratio_aspect: bool 16 | /// stretched: bool 17 | /// style: String 18 | /// ``` 19 | pub struct ImageState { 20 | data: String, 21 | extension: String, 22 | background: String, 23 | keep_ratio_aspect: bool, 24 | stretched: bool, 25 | style: String, 26 | } 27 | 28 | impl ImageState { 29 | /// Get the base64 encoded image data 30 | pub fn data(&self) -> &str { 31 | &self.data 32 | } 33 | 34 | /// Get the extension 35 | pub fn extension(&self) -> &str { 36 | &self.extension 37 | } 38 | 39 | /// Get the background color 40 | pub fn background(&self) -> &str { 41 | &self.background 42 | } 43 | 44 | /// Get the keep_ratio_aspect flag 45 | pub fn keep_ratio_aspect(&self) -> bool { 46 | self.keep_ratio_aspect 47 | } 48 | 49 | /// Get the stretched flag 50 | pub fn stretched(&self) -> bool { 51 | self.stretched 52 | } 53 | 54 | /// Get the style 55 | pub fn style(&self) -> &str { 56 | &self.style 57 | } 58 | 59 | /// Set the base64 encoded image data 60 | pub fn set_data(&mut self, data: &str) { 61 | self.data = data.to_string(); 62 | } 63 | 64 | /// Set the extension 65 | pub fn set_extension(&mut self, extension: &str) { 66 | self.extension = extension.to_string(); 67 | } 68 | 69 | /// Set the background color 70 | pub fn set_background(&mut self, background: &str) { 71 | self.background = background.to_string(); 72 | } 73 | 74 | /// Set the keep_ratio_aspect flag 75 | pub fn set_keep_ratio_aspect(&mut self, keep_ratio_aspect: bool) { 76 | self.keep_ratio_aspect = keep_ratio_aspect; 77 | } 78 | 79 | /// Set the stretched flag 80 | pub fn set_stretched(&mut self, stretched: bool) { 81 | self.stretched = stretched; 82 | } 83 | 84 | /// Set the style 85 | pub fn set_style(&mut self, style: &str) { 86 | self.style = style.to_string(); 87 | } 88 | } 89 | 90 | /// # The listener for an Image 91 | pub trait ImageListener { 92 | /// Function triggered on update event 93 | fn on_update(&self, state: &mut ImageState); 94 | } 95 | 96 | /// # An element able to display images from icons and path 97 | /// 98 | /// ## Fields 99 | /// 100 | /// ```text 101 | /// name: String 102 | /// state: ImageState 103 | /// listener: Option> 104 | /// ``` 105 | /// 106 | /// ## Default values 107 | /// 108 | /// The variable `pixmap` is built in both constructors from the given Icon or 109 | /// path. 110 | /// 111 | /// ```text 112 | /// name: name.to_string() 113 | /// state: 114 | /// data: pixmap.data().to_string() 115 | /// extension: pixmap.extension().to_string() 116 | /// background: "black".to_string() 117 | /// keep_ratio_aspect: false 118 | /// stretched: false 119 | /// style: "".to_string() 120 | /// listener: None 121 | /// ``` 122 | /// 123 | /// ## Style 124 | /// 125 | /// ```text 126 | /// div.image 127 | /// img 128 | /// ``` 129 | /// 130 | /// ## Example 131 | /// 132 | /// ``` 133 | /// use std::cell::RefCell; 134 | /// use std::rc::Rc; 135 | /// 136 | /// use neutrino::widgets::image::{Image, ImageListener, ImageState}; 137 | /// use neutrino::utils::theme::Theme; 138 | /// use neutrino::utils::pixmap::Pixmap; 139 | /// use neutrino::{App, Window}; 140 | /// 141 | /// 142 | /// struct Painting { 143 | /// path: String, 144 | /// } 145 | /// 146 | /// impl Painting { 147 | /// fn new(path: &str) -> Self { 148 | /// Self { path: path.to_string() } 149 | /// } 150 | /// 151 | /// fn path(&self) -> &str { 152 | /// &self.path 153 | /// } 154 | /// } 155 | /// 156 | /// 157 | /// struct MyImageListener { 158 | /// painting: Rc>, 159 | /// } 160 | /// 161 | /// impl MyImageListener { 162 | /// pub fn new(painting: Rc>) -> Self { 163 | /// Self { painting } 164 | /// } 165 | /// } 166 | /// 167 | /// impl ImageListener for MyImageListener { 168 | /// fn on_update(&self, state: &mut ImageState) { 169 | /// let pixmap = Pixmap::from_path(self.painting.borrow().path()); 170 | /// state.set_data(pixmap.data()); 171 | /// state.set_extension(pixmap.extension()); 172 | /// } 173 | /// } 174 | /// 175 | /// 176 | /// fn main() { 177 | /// let painting = Rc::new(RefCell::new( 178 | /// Painting::new("/home/neutrino/le_radeau_de_la_meduse.jpg") 179 | /// )); 180 | /// 181 | /// let my_listener = MyImageListener::new(Rc::clone(&painting)); 182 | /// 183 | /// let mut my_image = Image::from_path( 184 | /// "my_image", 185 | /// "/home/neutrino/le_radeau_de_la_meduse.jpg" 186 | /// ); 187 | /// my_image.set_listener(Box::new(my_listener)); 188 | /// } 189 | /// ``` 190 | pub struct Image { 191 | name: String, 192 | state: ImageState, 193 | listener: Option>, 194 | } 195 | 196 | impl Image { 197 | /// Create an image from a path 198 | pub fn from_path(name: &str, path: &str) -> Self { 199 | let pixmap = Pixmap::from_path(path); 200 | Self { 201 | name: name.to_string(), 202 | state: ImageState { 203 | data: pixmap.data().to_string(), 204 | extension: pixmap.extension().to_string(), 205 | background: "black".to_string(), 206 | keep_ratio_aspect: false, 207 | stretched: false, 208 | style: "".to_string(), 209 | }, 210 | listener: None, 211 | } 212 | } 213 | 214 | /// Create an image from an icon 215 | pub fn from_icon(name: &str, icon: Box) -> Self { 216 | let pixmap = Pixmap::from_icon(icon); 217 | Self { 218 | name: name.to_string(), 219 | state: ImageState { 220 | data: pixmap.data().to_string(), 221 | extension: pixmap.extension().to_string(), 222 | background: "black".to_string(), 223 | keep_ratio_aspect: false, 224 | stretched: false, 225 | style: "".to_string(), 226 | }, 227 | listener: None, 228 | } 229 | } 230 | 231 | /// Set the background color 232 | pub fn set_background(&mut self, background: &str) { 233 | self.state.set_background(background); 234 | } 235 | 236 | /// Set the keep_ratio_aspect flag to true 237 | pub fn set_keep_ratio_aspect(&mut self) { 238 | self.state.set_keep_ratio_aspect(true); 239 | } 240 | 241 | /// Set the stretched flag to true 242 | pub fn set_stretched(&mut self) { 243 | self.state.set_stretched(true); 244 | } 245 | 246 | /// Set the listener 247 | pub fn set_listener(&mut self, listener: Box) { 248 | self.listener = Some(listener); 249 | } 250 | 251 | /// Set the style 252 | pub fn set_style(&mut self, style: &str) { 253 | self.state.set_style(style); 254 | } 255 | } 256 | 257 | impl Widget for Image { 258 | fn eval(&self) -> String { 259 | let ratio = if self.state.keep_ratio_aspect() { 260 | "" 261 | } else { 262 | r#"width="100%" height="100%""# 263 | }; 264 | let stretched = if self.state.stretched() { 265 | "stretched" 266 | } else { 267 | "" 268 | }; 269 | let style = inline_style(&scss_to_css(&format!( 270 | r##"#{}{{{}}}"##, 271 | self.name, 272 | self.state.style(), 273 | ))); 274 | let html = format!( 275 | r#" 276 |
277 | 278 |
279 | "#, 280 | self.name, 281 | stretched, 282 | self.state.background(), 283 | ratio, 284 | self.state.extension(), 285 | self.state.data(), 286 | ); 287 | format!("{}{}", style, html) 288 | } 289 | 290 | fn trigger(&mut self, event: &Event) { 291 | match event { 292 | Event::Update => self.on_update(), 293 | Event::Change { source, value } => { 294 | if source == &self.name { 295 | self.on_change(value) 296 | } 297 | } 298 | _ => (), 299 | } 300 | } 301 | 302 | fn on_update(&mut self) { 303 | match &self.listener { 304 | None => (), 305 | Some(listener) => { 306 | listener.on_update(&mut self.state); 307 | } 308 | } 309 | } 310 | 311 | fn on_change(&mut self, _value: &str) {} 312 | } 313 | -------------------------------------------------------------------------------- /src/widgets/label.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::style::{inline_style, scss_to_css}; 3 | use crate::widgets::widget::Widget; 4 | 5 | /// # The state of a Label 6 | /// 7 | /// ## Fields 8 | /// 9 | /// ```text 10 | /// text: String 11 | /// stretched: bool 12 | /// unselectable: bool 13 | /// style: String 14 | /// ``` 15 | pub struct LabelState { 16 | text: String, 17 | stretched: bool, 18 | unselectable: bool, 19 | style: String, 20 | } 21 | 22 | impl LabelState { 23 | /// Get the text 24 | pub fn text(&self) -> &str { 25 | &self.text 26 | } 27 | 28 | /// Get the stretched flag 29 | pub fn stretched(&self) -> bool { 30 | self.stretched 31 | } 32 | 33 | /// Get the unselectable flag 34 | pub fn unselectable(&self) -> bool { 35 | self.unselectable 36 | } 37 | 38 | /// Get the style 39 | pub fn style(&self) -> &str { 40 | &self.style 41 | } 42 | 43 | /// Set the text 44 | pub fn set_text(&mut self, text: &str) { 45 | self.text = text.to_string(); 46 | } 47 | 48 | /// Set the stretched flag 49 | pub fn set_stretched(&mut self, stretched: bool) { 50 | self.stretched = stretched; 51 | } 52 | 53 | /// Set the uselectable flag 54 | pub fn set_unselectable(&mut self, unselectable: bool) { 55 | self.unselectable = unselectable; 56 | } 57 | 58 | /// Set the style 59 | pub fn set_style(&mut self, style: &str) { 60 | self.style = style.to_string(); 61 | } 62 | } 63 | 64 | /// # The listener of a Label 65 | pub trait LabelListener { 66 | /// Function triggered on update event 67 | fn on_update(&self, state: &mut LabelState); 68 | } 69 | 70 | /// # An element able to display text 71 | /// 72 | /// ## Fields 73 | /// 74 | /// ```text 75 | /// name: String 76 | /// state: LabelState 77 | /// listener: Option> 78 | /// ``` 79 | /// 80 | /// ## Default values 81 | /// 82 | /// ```text 83 | /// name: name.to_string() 84 | /// state: 85 | /// text: "Label".to_string() 86 | /// stretched: false 87 | /// unselectable: false 88 | /// style: "".to_string() 89 | /// listener: None 90 | /// ``` 91 | /// 92 | /// ## Style 93 | /// 94 | /// ```text 95 | /// div.label 96 | /// ``` 97 | /// 98 | /// ## Example 99 | /// 100 | /// ``` 101 | /// use std::cell::RefCell; 102 | /// use std::rc::Rc; 103 | /// 104 | /// use neutrino::widgets::label::{Label, LabelListener, LabelState}; 105 | /// use neutrino::utils::theme::Theme; 106 | /// use neutrino::{App, Window}; 107 | /// 108 | /// 109 | /// struct Paragraph { 110 | /// text: String, 111 | /// } 112 | /// 113 | /// impl Paragraph { 114 | /// fn new() -> Self { 115 | /// Self { text: "".to_string() } 116 | /// } 117 | /// 118 | /// fn text(&self) -> &str { 119 | /// &self.text 120 | /// } 121 | /// } 122 | /// 123 | /// 124 | /// struct MyLabelListener { 125 | /// paragraph: Rc>, 126 | /// } 127 | /// 128 | /// impl MyLabelListener { 129 | /// pub fn new(paragraph: Rc>) -> Self { 130 | /// Self { paragraph } 131 | /// } 132 | /// } 133 | /// 134 | /// impl LabelListener for MyLabelListener { 135 | /// fn on_update(&self, state: &mut LabelState) { 136 | /// state.set_text(self.paragraph.borrow().text()); 137 | /// } 138 | /// } 139 | /// 140 | /// 141 | /// fn main() { 142 | /// let paragraph = Rc::new(RefCell::new(Paragraph::new())); 143 | /// 144 | /// let my_listener = MyLabelListener::new(Rc::clone(¶graph)); 145 | /// 146 | /// let mut my_label = Label::new("my_label"); 147 | /// my_label.set_text("Hello world!"); 148 | /// my_label.set_listener(Box::new(my_listener)); 149 | /// } 150 | /// ``` 151 | pub struct Label { 152 | name: String, 153 | state: LabelState, 154 | listener: Option>, 155 | } 156 | 157 | impl Label { 158 | /// Create a Label 159 | pub fn new(name: &str) -> Self { 160 | Self { 161 | name: name.to_string(), 162 | state: LabelState { 163 | text: "Label".to_string(), 164 | stretched: false, 165 | unselectable: false, 166 | style: "".to_string(), 167 | }, 168 | listener: None, 169 | } 170 | } 171 | 172 | /// Set the text 173 | pub fn set_text(&mut self, text: &str) { 174 | self.state.set_text(text); 175 | } 176 | 177 | /// Set the stretched flag to true 178 | pub fn set_stretched(&mut self) { 179 | self.state.set_stretched(true); 180 | } 181 | 182 | /// Set the unselectable flag to true 183 | pub fn set_unselectable(&mut self) { 184 | self.state.set_unselectable(true); 185 | } 186 | 187 | /// Set the listener 188 | pub fn set_listener(&mut self, listener: Box) { 189 | self.listener = Some(listener); 190 | } 191 | 192 | /// Set the style 193 | pub fn set_style(&mut self, style: &str) { 194 | self.state.set_style(style); 195 | } 196 | } 197 | 198 | impl Widget for Label { 199 | fn eval(&self) -> String { 200 | let stretched = if self.state.stretched() { 201 | "stretched" 202 | } else { 203 | "" 204 | }; 205 | let user_select_class = if self.state.unselectable() { 206 | "unselectable" 207 | } else { 208 | "selectable" 209 | }; 210 | let style = inline_style(&scss_to_css(&format!( 211 | r##"#{}{{{}}}"##, 212 | self.name, 213 | self.state.style(), 214 | ))); 215 | let html = format!( 216 | r#"
{}
"#, 217 | self.name, 218 | stretched, 219 | user_select_class, 220 | self.state.text() 221 | ); 222 | format!("{}{}", style, html) 223 | } 224 | 225 | fn trigger(&mut self, event: &Event) { 226 | match event { 227 | Event::Update => self.on_update(), 228 | Event::Change { source, value } => { 229 | if source == &self.name { 230 | self.on_change(value) 231 | } 232 | } 233 | _ => (), 234 | } 235 | } 236 | 237 | fn on_update(&mut self) { 238 | match &self.listener { 239 | None => (), 240 | Some(listener) => { 241 | listener.on_update(&mut self.state); 242 | } 243 | } 244 | } 245 | 246 | fn on_change(&mut self, _value: &str) {} 247 | } 248 | -------------------------------------------------------------------------------- /src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod button; 2 | pub mod checkbox; 3 | pub mod combo; 4 | pub mod container; 5 | pub mod image; 6 | pub mod label; 7 | pub mod menubar; 8 | pub mod progressbar; 9 | pub mod radio; 10 | pub mod range; 11 | pub mod tabs; 12 | pub mod textinput; 13 | pub mod widget; 14 | -------------------------------------------------------------------------------- /src/widgets/progressbar.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::style::{inline_style, scss_to_css}; 3 | use crate::widgets::widget::Widget; 4 | 5 | /// # The state of a ProgressBar 6 | /// 7 | /// ## Fields 8 | /// 9 | /// ```text 10 | /// min: i32 11 | /// max: i32 12 | /// value: i32 13 | /// stretched: bool 14 | /// style: String 15 | /// ``` 16 | pub struct ProgressBarState { 17 | min: i32, 18 | max: i32, 19 | value: i32, 20 | stretched: bool, 21 | style: String, 22 | } 23 | 24 | impl ProgressBarState { 25 | /// Get the min 26 | pub fn min(&self) -> i32 { 27 | self.min 28 | } 29 | 30 | /// Get the max 31 | pub fn max(&self) -> i32 { 32 | self.max 33 | } 34 | 35 | /// Get the value 36 | pub fn value(&self) -> i32 { 37 | self.value 38 | } 39 | 40 | /// Get the stretched flag 41 | pub fn stretched(&self) -> bool { 42 | self.stretched 43 | } 44 | 45 | /// Get the style 46 | pub fn style(&self) -> &str { 47 | &self.style 48 | } 49 | 50 | /// Set the min 51 | pub fn set_min(&mut self, min: i32) { 52 | self.min = min; 53 | } 54 | 55 | /// Set the max 56 | pub fn set_max(&mut self, max: i32) { 57 | self.max = max; 58 | } 59 | 60 | /// Set the value 61 | pub fn set_value(&mut self, value: i32) { 62 | self.value = if value > self.max { 63 | self.max 64 | } else if value < self.min { 65 | self.min 66 | } else { 67 | value 68 | }; 69 | } 70 | 71 | /// Set the stretched flqg 72 | pub fn set_stretched(&mut self, stretched: bool) { 73 | self.stretched = stretched; 74 | } 75 | 76 | /// Set the style 77 | pub fn set_style(&mut self, style: &str) { 78 | self.style = style.to_string(); 79 | } 80 | } 81 | 82 | /// # The listener of a ProgressBar 83 | pub trait ProgressBarListener { 84 | /// Function triggered on update event 85 | fn on_update(&self, state: &mut ProgressBarState); 86 | } 87 | 88 | /// # A progress bar 89 | /// 90 | /// ## Fields 91 | /// 92 | /// ```text 93 | /// name: String 94 | /// state: ProgressBarState 95 | /// listener: Option> 96 | /// ``` 97 | /// 98 | /// ## Default values 99 | /// 100 | /// ```text 101 | /// name: name.to_string() 102 | /// state: 103 | /// min: 0 104 | /// max: 100 105 | /// value: 0 106 | /// stretched: false 107 | /// style: "".to_string() 108 | /// listener: None 109 | /// ``` 110 | /// 111 | /// ## Style 112 | /// 113 | /// ```text 114 | /// div.progressbar 115 | /// div.background 116 | /// div.foreground 117 | /// ``` 118 | /// 119 | /// ## Example 120 | /// 121 | /// ``` 122 | /// use std::cell::RefCell; 123 | /// use std::rc::Rc; 124 | /// 125 | /// use neutrino::widgets::progressbar::{ 126 | /// ProgressBar, 127 | /// ProgressBarListener, 128 | /// ProgressBarState 129 | /// }; 130 | /// use neutrino::utils::theme::Theme; 131 | /// use neutrino::{App, Window}; 132 | /// 133 | /// 134 | /// struct Counter { 135 | /// value: i32, 136 | /// } 137 | /// 138 | /// impl Counter { 139 | /// fn new() -> Self { 140 | /// Self { value: 0 } 141 | /// } 142 | /// 143 | /// fn value(&self) -> i32 { 144 | /// self.value 145 | /// } 146 | /// } 147 | /// 148 | /// 149 | /// struct MyProgressBarListener { 150 | /// counter: Rc>, 151 | /// } 152 | /// 153 | /// impl MyProgressBarListener { 154 | /// pub fn new(counter: Rc>) -> Self { 155 | /// Self { counter } 156 | /// } 157 | /// } 158 | /// 159 | /// impl ProgressBarListener for MyProgressBarListener { 160 | /// fn on_update(&self, state: &mut ProgressBarState) { 161 | /// state.set_value(self.counter.borrow().value()); 162 | /// } 163 | /// } 164 | /// 165 | /// 166 | /// fn main() { 167 | /// let counter = Rc::new(RefCell::new(Counter::new())); 168 | /// 169 | /// let my_listener = MyProgressBarListener::new(Rc::clone(&counter)); 170 | /// 171 | /// let mut my_progressbar = ProgressBar::new("my_progressbar"); 172 | /// my_progressbar.set_listener(Box::new(my_listener)); 173 | /// } 174 | /// ``` 175 | pub struct ProgressBar { 176 | name: String, 177 | state: ProgressBarState, 178 | listener: Option>, 179 | } 180 | 181 | impl ProgressBar { 182 | /// Create a ProgressBar 183 | pub fn new(name: &str) -> Self { 184 | Self { 185 | name: name.to_string(), 186 | state: ProgressBarState { 187 | min: 0, 188 | max: 100, 189 | value: 0, 190 | stretched: false, 191 | style: "".to_string(), 192 | }, 193 | listener: None, 194 | } 195 | } 196 | 197 | // Set the min 198 | pub fn set_min(&mut self, min: i32) { 199 | self.state.set_min(min); 200 | } 201 | 202 | // Set the max 203 | pub fn set_max(&mut self, max: i32) { 204 | self.state.set_max(max); 205 | } 206 | 207 | // Set the value 208 | pub fn set_value(&mut self, value: i32) { 209 | self.state.set_value(value); 210 | } 211 | 212 | // Set the stretched flag to true 213 | pub fn set_stretched(&mut self) { 214 | self.state.set_stretched(true); 215 | } 216 | 217 | /// Set the listener 218 | pub fn set_listener(&mut self, listener: Box) { 219 | self.listener = Some(listener); 220 | } 221 | 222 | /// Set the style 223 | pub fn set_style(&mut self, style: &str) { 224 | self.state.set_style(style); 225 | } 226 | } 227 | 228 | impl Widget for ProgressBar { 229 | fn eval(&self) -> String { 230 | let stretched = if self.state.stretched() { 231 | "stretched" 232 | } else { 233 | "" 234 | }; 235 | let style = inline_style(&scss_to_css(&format!( 236 | r##"#{}{{{}}}"##, 237 | self.name, 238 | self.state.style(), 239 | ))); 240 | let html = format!( 241 | r#" 242 |
243 |
244 |
245 |
246 | "#, 247 | self.name, 248 | stretched, 249 | f64::from(self.state.value() - self.state.min()) / 250 | f64::from(self.state.max() - self.state.min()) * 251 | 100.0, 252 | ); 253 | format!("{}{}", style, html) 254 | } 255 | 256 | fn trigger(&mut self, event: &Event) { 257 | match event { 258 | Event::Update => self.on_update(), 259 | Event::Change { source, value } => { 260 | if source == &self.name { 261 | self.on_change(value) 262 | } 263 | } 264 | _ => (), 265 | } 266 | } 267 | 268 | fn on_update(&mut self) { 269 | match &self.listener { 270 | None => (), 271 | Some(listener) => { 272 | listener.on_update(&mut self.state); 273 | } 274 | } 275 | } 276 | 277 | fn on_change(&mut self, _value: &str) {} 278 | } 279 | -------------------------------------------------------------------------------- /src/widgets/radio.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::style::{inline_style, scss_to_css}; 3 | use crate::widgets::widget::Widget; 4 | 5 | /// # The state of a Radio 6 | /// 7 | /// ## Fields 8 | /// 9 | /// ```text 10 | /// choices: Vec 11 | /// selected: u32 12 | /// disabled: bool 13 | /// stretched: bool 14 | /// style: String 15 | /// ``` 16 | pub struct RadioState { 17 | choices: Vec, 18 | selected: u32, 19 | disabled: bool, 20 | stretched: bool, 21 | style: String, 22 | } 23 | 24 | impl RadioState { 25 | /// Get the choices 26 | pub fn choices(&self) -> &Vec { 27 | &self.choices 28 | } 29 | 30 | /// Get the selected index 31 | pub fn selected(&self) -> u32 { 32 | self.selected 33 | } 34 | 35 | /// Get the disabled flag 36 | pub fn disabled(&self) -> bool { 37 | self.disabled 38 | } 39 | 40 | /// Get the stretched flag 41 | pub fn stretched(&self) -> bool { 42 | self.stretched 43 | } 44 | 45 | /// Get the style 46 | pub fn style(&self) -> &str { 47 | &self.style 48 | } 49 | 50 | /// Set the choices 51 | pub fn set_choices(&mut self, choices: Vec<&str>) { 52 | self.choices = choices 53 | .iter() 54 | .map(|c| c.to_string()) 55 | .collect::>(); 56 | } 57 | 58 | /// Set the selected index 59 | pub fn set_selected(&mut self, selected: u32) { 60 | self.selected = selected; 61 | } 62 | 63 | /// Set the disabled flag 64 | pub fn set_disabled(&mut self, disabled: bool) { 65 | self.disabled = disabled; 66 | } 67 | 68 | /// Set the stretched flag 69 | pub fn set_stretched(&mut self, stretched: bool) { 70 | self.stretched = stretched; 71 | } 72 | 73 | /// Set the style 74 | pub fn set_style(&mut self, style: &str) { 75 | self.style = style.to_string(); 76 | } 77 | } 78 | 79 | /// # The listener of a Radio 80 | pub trait RadioListener { 81 | /// Function triggered on change event 82 | fn on_change(&self, state: &RadioState); 83 | 84 | /// Function triggered on update event 85 | fn on_update(&self, state: &mut RadioState); 86 | } 87 | 88 | /// # A list of radio buttons 89 | /// 90 | /// Only one can be selected at a time. 91 | /// 92 | /// ## Fields 93 | /// 94 | /// ```text 95 | /// name: String 96 | /// state: RadioState 97 | /// listener: Option> 98 | /// ``` 99 | /// 100 | /// ## Default values 101 | /// 102 | /// ```text 103 | /// name: name.to_string() 104 | /// state: 105 | /// choices: vec!["Choice 1".to_string(), "Choice 2".to_string()], 106 | /// selected: 0 107 | /// disabled: false 108 | /// stretched: false 109 | /// style: "".to_string() 110 | /// listener: None 111 | /// ``` 112 | /// 113 | /// ## Style 114 | /// 115 | /// ```text 116 | /// div.radio[.disabled][.selected] 117 | /// label 118 | /// div.radio-outer 119 | /// div.radio-inner 120 | /// ``` 121 | /// 122 | /// ## Example 123 | /// 124 | /// ``` 125 | /// use std::cell::RefCell; 126 | /// use std::rc::Rc; 127 | /// 128 | /// use neutrino::widgets::radio::{Radio, RadioListener, RadioState}; 129 | /// use neutrino::utils::theme::Theme; 130 | /// use neutrino::{App, Window}; 131 | /// 132 | /// 133 | /// struct Dessert { 134 | /// index: u32, 135 | /// value: String, 136 | /// } 137 | /// 138 | /// impl Dessert { 139 | /// fn new() -> Self { 140 | /// Self { index: 0, value: "Cake".to_string() } 141 | /// } 142 | /// 143 | /// fn index(&self) -> u32 { 144 | /// self.index 145 | /// } 146 | /// 147 | /// fn value(&self) -> &str { 148 | /// &self.value 149 | /// } 150 | /// 151 | /// fn set(&mut self, index: u32, value: &str) { 152 | /// self.index = index; 153 | /// self.value = value.to_string(); 154 | /// } 155 | /// } 156 | /// 157 | /// 158 | /// struct MyRadioListener { 159 | /// dessert: Rc>, 160 | /// } 161 | /// 162 | /// impl MyRadioListener { 163 | /// pub fn new(dessert: Rc>) -> Self { 164 | /// Self { dessert } 165 | /// } 166 | /// } 167 | /// 168 | /// impl RadioListener for MyRadioListener { 169 | /// fn on_change(&self, state: &RadioState) { 170 | /// let index = state.selected(); 171 | /// self.dessert.borrow_mut().set( 172 | /// index, 173 | /// &state.choices()[index as usize] 174 | /// ); 175 | /// } 176 | /// 177 | /// fn on_update(&self, state: &mut RadioState) { 178 | /// state.set_selected(self.dessert.borrow().index()); 179 | /// } 180 | /// } 181 | /// 182 | /// 183 | /// fn main() { 184 | /// let dessert = Rc::new(RefCell::new(Dessert::new())); 185 | /// 186 | /// let my_listener = MyRadioListener::new(Rc::clone(&dessert)); 187 | /// 188 | /// let mut my_radio = Radio::new("my_radio"); 189 | /// my_radio.set_choices(vec!["Cake", "Ice Cream", "Pie"]); 190 | /// my_radio.set_listener(Box::new(my_listener)); 191 | /// } 192 | /// ``` 193 | pub struct Radio { 194 | name: String, 195 | state: RadioState, 196 | listener: Option>, 197 | } 198 | 199 | impl Radio { 200 | /// Create a Radio 201 | pub fn new(name: &str) -> Self { 202 | Self { 203 | name: name.to_string(), 204 | state: RadioState { 205 | choices: vec![ 206 | "Choice 1".to_string(), 207 | "Choice 2".to_string() 208 | ], 209 | selected: 0, 210 | disabled: false, 211 | stretched: false, 212 | style: "".to_string(), 213 | }, 214 | listener: None, 215 | } 216 | } 217 | 218 | /// Set the choices 219 | pub fn set_choices(&mut self, choices: Vec<&str>) { 220 | self.state.set_choices(choices); 221 | } 222 | 223 | /// Set the selected index 224 | pub fn set_selected(&mut self, selected: u32) { 225 | self.state.set_selected(selected); 226 | } 227 | 228 | /// Set the disabled flag to true 229 | pub fn set_disabled(&mut self) { 230 | self.state.set_disabled(true); 231 | } 232 | 233 | /// Set the stretched flag 234 | pub fn set_stretched(&mut self) { 235 | self.state.set_stretched(true); 236 | } 237 | 238 | /// Set the listener 239 | pub fn set_listener(&mut self, listener: Box) { 240 | self.listener = Some(listener); 241 | } 242 | 243 | /// Set the style 244 | pub fn set_style(&mut self, style: &str) { 245 | self.state.set_style(style); 246 | } 247 | } 248 | 249 | impl Widget for Radio { 250 | fn eval(&self) -> String { 251 | let stretched = if self.state.stretched() { 252 | "stretched" 253 | } else { 254 | "" 255 | }; 256 | let disabled = if self.state.disabled() { 257 | "disabled" 258 | } else { 259 | "" 260 | }; 261 | let style = inline_style(&scss_to_css(&format!( 262 | r##"#{}{{{}}}"##, 263 | self.name, 264 | self.state.style(), 265 | ))); 266 | let mut html = "".to_string(); 267 | for (i, choice) in self.state.choices().iter().enumerate() { 268 | let selected = if self.state.selected() == i as u32 { 269 | "selected" 270 | } else { 271 | "" 272 | }; 273 | html.push_str( 274 | &format!( 275 | r#" 276 |
277 |
278 |
279 |
280 | 281 |
282 | "#, 283 | self.name, 284 | stretched, 285 | disabled, 286 | selected, 287 | Event::change_js(&self.name, &format!("'{}'", i)), 288 | choice 289 | ) 290 | ); 291 | } 292 | format!("{}{}", style, html) 293 | } 294 | 295 | fn trigger(&mut self, event: &Event) { 296 | match event { 297 | Event::Update => self.on_update(), 298 | Event::Change { source, value } => { 299 | if source == &self.name && !self.state.disabled { 300 | self.on_change(value); 301 | } 302 | } 303 | _ => (), 304 | } 305 | } 306 | 307 | fn on_update(&mut self) { 308 | match &self.listener { 309 | None => (), 310 | Some(listener) => { 311 | listener.on_update(&mut self.state); 312 | } 313 | } 314 | } 315 | 316 | fn on_change(&mut self, value: &str) { 317 | self.state.set_selected(value.parse::().unwrap()); 318 | match &self.listener { 319 | None => (), 320 | Some(listener) => { 321 | listener.on_change(&self.state); 322 | } 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/widgets/range.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::style::{inline_style, scss_to_css}; 3 | use crate::widgets::widget::Widget; 4 | 5 | /// # The state of a Range 6 | /// 7 | /// ## Fields 8 | /// 9 | /// ```text 10 | /// min: i32 11 | /// max: i32 12 | /// value: i32 13 | /// disabled: bool 14 | /// stretched: bool 15 | /// style: String 16 | /// ``` 17 | pub struct RangeState { 18 | min: i32, 19 | max: i32, 20 | value: i32, 21 | disabled: bool, 22 | stretched: bool, 23 | style: String, 24 | } 25 | 26 | impl RangeState { 27 | /// Get the min 28 | pub fn min(&self) -> i32 { 29 | self.min 30 | } 31 | 32 | /// Get the max 33 | pub fn max(&self) -> i32 { 34 | self.max 35 | } 36 | 37 | /// Get the value 38 | pub fn value(&self) -> i32 { 39 | self.value 40 | } 41 | 42 | /// Get the disabled flag 43 | pub fn disabled(&self) -> bool { 44 | self.disabled 45 | } 46 | 47 | /// Get the stretched flag 48 | pub fn stretched(&self) -> bool { 49 | self.stretched 50 | } 51 | 52 | /// Get the style 53 | pub fn style(&self) -> &str { 54 | &self.style 55 | } 56 | 57 | /// Set the min 58 | pub fn set_min(&mut self, min: i32) { 59 | self.min = min; 60 | } 61 | 62 | /// Set the max 63 | pub fn set_max(&mut self, max: i32) { 64 | self.max = max; 65 | } 66 | 67 | /// Set the value 68 | pub fn set_value(&mut self, value: i32) { 69 | self.value = value; 70 | } 71 | 72 | /// Set the disabled flag 73 | pub fn set_disabled(&mut self, disabled: bool) { 74 | self.disabled = disabled; 75 | } 76 | 77 | /// Set the stretched flag 78 | pub fn set_stretched(&mut self, stretched: bool) { 79 | self.stretched = stretched; 80 | } 81 | 82 | /// Set the style 83 | pub fn set_style(&mut self, style: &str) { 84 | self.style = style.to_string(); 85 | } 86 | } 87 | 88 | /// # The listener of a Range 89 | pub trait RangeListener { 90 | /// Function triggered on update event 91 | fn on_update(&self, state: &mut RangeState); 92 | 93 | /// Function triggered on change event 94 | fn on_change(&self, state: &RangeState); 95 | } 96 | 97 | /// # A progress bar with a handle 98 | /// 99 | /// ## Fields 100 | /// 101 | /// ```text 102 | /// name: String 103 | /// state: RangeState 104 | /// listener: Option> 105 | /// ``` 106 | /// 107 | /// ## Default values 108 | /// 109 | /// ```text 110 | /// name: name.to_string() 111 | /// state: 112 | /// min: 0 113 | /// max: 100 114 | /// value: 0 115 | /// disabled: false 116 | /// stretched: false 117 | /// style: "".to_string() 118 | /// listener: None 119 | /// ``` 120 | /// 121 | /// ## Style 122 | /// 123 | /// ```text 124 | /// div.range[.disabled] 125 | /// div.inner-range 126 | /// ::-webkit-slider-runnable-track 127 | /// ::-webkit-slider-thumb 128 | /// ::-ms-track 129 | /// ::-ms-thumb 130 | /// ``` 131 | /// 132 | /// ## Example 133 | /// 134 | /// ``` 135 | /// use std::cell::RefCell; 136 | /// use std::rc::Rc; 137 | /// 138 | /// use neutrino::widgets::range::{Range, RangeListener, RangeState}; 139 | /// use neutrino::utils::theme::Theme; 140 | /// use neutrino::{App, Window}; 141 | /// 142 | /// 143 | /// struct Counter { 144 | /// value: i32, 145 | /// } 146 | /// 147 | /// impl Counter { 148 | /// fn new() -> Self { 149 | /// Self { value: 0 } 150 | /// } 151 | /// 152 | /// fn value(&self) -> i32 { 153 | /// self.value 154 | /// } 155 | /// 156 | /// fn set_value(&mut self, value: i32) { 157 | /// self.value = value; 158 | /// } 159 | /// } 160 | /// 161 | /// 162 | /// struct MyRangeListener { 163 | /// counter: Rc>, 164 | /// } 165 | /// 166 | /// impl MyRangeListener { 167 | /// pub fn new(counter: Rc>) -> Self { 168 | /// Self { counter } 169 | /// } 170 | /// } 171 | /// 172 | /// impl RangeListener for MyRangeListener { 173 | /// fn on_change(&self, state: &RangeState) { 174 | /// self.counter.borrow_mut().set_value(state.value()); 175 | /// } 176 | /// 177 | /// fn on_update(&self, state: &mut RangeState) { 178 | /// state.set_value(self.counter.borrow().value()); 179 | /// } 180 | /// } 181 | /// 182 | /// 183 | /// fn main() { 184 | /// let counter = Rc::new(RefCell::new(Counter::new())); 185 | /// 186 | /// let my_listener = MyRangeListener::new(Rc::clone(&counter)); 187 | /// 188 | /// let mut my_range = Range::new("my_range"); 189 | /// my_range.set_listener(Box::new(my_listener)); 190 | /// } 191 | /// ``` 192 | pub struct Range { 193 | name: String, 194 | state: RangeState, 195 | listener: Option>, 196 | } 197 | 198 | impl Range { 199 | /// Create a Range 200 | pub fn new(name: &str) -> Self { 201 | Self { 202 | name: name.to_string(), 203 | state: RangeState { 204 | min: 0, 205 | max: 100, 206 | value: 0, 207 | disabled: false, 208 | stretched: false, 209 | style: "".to_string(), 210 | }, 211 | listener: None, 212 | } 213 | } 214 | 215 | /// Set the min 216 | pub fn set_min(&mut self, min: i32) { 217 | self.state.set_min(min); 218 | } 219 | 220 | /// Set the max 221 | pub fn set_max(&mut self, max: i32) { 222 | self.state.set_max(max); 223 | } 224 | 225 | /// Set the value 226 | pub fn set_value(&mut self, value: i32) { 227 | self.state.set_value(value); 228 | } 229 | 230 | /// Set the disabled flag to true 231 | pub fn set_disabled(&mut self) { 232 | self.state.set_disabled(true); 233 | } 234 | 235 | /// Set the stretched flag to true 236 | pub fn set_stretched(&mut self) { 237 | self.state.set_stretched(true); 238 | } 239 | 240 | /// Set the listener 241 | pub fn set_listener(&mut self, listener: Box) { 242 | self.listener = Some(listener); 243 | } 244 | 245 | /// Set the style 246 | pub fn set_style(&mut self, style: &str) { 247 | self.state.set_style(style); 248 | } 249 | } 250 | 251 | impl Widget for Range { 252 | fn eval(&self) -> String { 253 | let stretched = if self.state.stretched() { 254 | "stretched" 255 | } else { 256 | "" 257 | }; 258 | let disabled = if self.state.disabled() { 259 | "disabled" 260 | } else { 261 | "" 262 | }; 263 | let style = inline_style(&scss_to_css(&format!( 264 | r##"#{}{{{}}}"##, 265 | self.name, 266 | self.state.style(), 267 | ))); 268 | let html = format!( 269 | r#" 270 |
271 | 274 |
275 | "#, 276 | self.name, 277 | disabled, 278 | stretched, 279 | disabled, 280 | Event::change_js(&self.name, "value"), 281 | Event::change_js(&self.name, "value"), 282 | self.state.min(), 283 | self.state.max(), 284 | self.state.value(), 285 | ); 286 | format!("{}{}", style, html) 287 | } 288 | 289 | fn trigger(&mut self, event: &Event) { 290 | match event { 291 | Event::Update => self.on_update(), 292 | Event::Change { source, value } => { 293 | if source == &self.name && !self.state.disabled() { 294 | self.on_change(value); 295 | } 296 | } 297 | _ => (), 298 | } 299 | } 300 | 301 | fn on_update(&mut self) { 302 | match &self.listener { 303 | None => (), 304 | Some(listener) => { 305 | listener.on_update(&mut self.state); 306 | } 307 | } 308 | } 309 | 310 | fn on_change(&mut self, value: &str) { 311 | self.state.set_value(value.parse::().unwrap()); 312 | match &self.listener { 313 | None => (), 314 | Some(listener) => { 315 | listener.on_change(&self.state); 316 | } 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/widgets/tabs.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::style::{inline_style, scss_to_css}; 3 | use crate::widgets::container::Direction; 4 | use crate::widgets::widget::Widget; 5 | 6 | /// # The state of a Tabs 7 | /// 8 | /// ## Fields 9 | /// 10 | /// ```text 11 | /// titles: Vec 12 | /// children: Vec> 13 | /// selected: u32 14 | /// direction: Direction 15 | /// stretched: bool 16 | /// style: String 17 | /// ``` 18 | pub struct TabsState { 19 | titles: Vec, 20 | children: Vec>, 21 | selected: u32, 22 | direction: Direction, 23 | stretched: bool, 24 | style: String, 25 | } 26 | 27 | impl TabsState { 28 | /// Get the titles 29 | pub fn titles(&self) -> &Vec { 30 | &self.titles 31 | } 32 | 33 | /// Get the children 34 | pub fn children(&self) -> &Vec> { 35 | &self.children 36 | } 37 | 38 | /// Get the selected index 39 | pub fn selected(&self) -> u32 { 40 | self.selected 41 | } 42 | 43 | /// Get the direction 44 | pub fn direction(&self) -> &Direction { 45 | &self.direction 46 | } 47 | 48 | /// Get the stretched flag 49 | pub fn stretched(&self) -> bool { 50 | self.stretched 51 | } 52 | 53 | /// Get the style 54 | pub fn style(&self) -> &str { 55 | &self.style 56 | } 57 | 58 | /// Set the titles 59 | pub fn set_titles(&mut self, titles: Vec<&str>) { 60 | self.titles = titles 61 | .iter() 62 | .map(|t| t.to_string()) 63 | .collect::>(); 64 | } 65 | 66 | /// Set the children 67 | pub fn set_children(&mut self, children: Vec>) { 68 | self.children = children; 69 | } 70 | 71 | /// Set the selected index 72 | pub fn set_selected(&mut self, selected: u32) { 73 | self.selected = selected; 74 | } 75 | 76 | /// Set the direction 77 | pub fn set_direction(&mut self, direction: Direction) { 78 | self.direction = direction; 79 | } 80 | 81 | /// Set the stretched flag 82 | pub fn set_stretched(&mut self, stretched: bool) { 83 | self.stretched = stretched; 84 | } 85 | 86 | /// Set the style 87 | pub fn set_style(&mut self, style: &str) { 88 | self.style = style.to_string(); 89 | } 90 | 91 | /// Add a tab 92 | fn add(&mut self, name: &str, child: Box) { 93 | self.titles.push(name.to_string()); 94 | self.children.push(child); 95 | } 96 | } 97 | 98 | /// # The listener of a Tabs 99 | pub trait TabsListener { 100 | /// Function triggered on update event 101 | fn on_update(&self, state: &mut TabsState); 102 | 103 | /// Function triggered on change event 104 | fn on_change(&self, state: &TabsState); 105 | } 106 | 107 | /// # A list of tabs 108 | /// 109 | /// ## Fields 110 | /// 111 | /// ```text 112 | /// name: String 113 | /// state: TabsState 114 | /// listener: Option> 115 | /// ``` 116 | /// 117 | /// ## Default values 118 | /// 119 | /// ```text 120 | /// name: name.to_string() 121 | /// state: 122 | /// title: vec![] 123 | /// children: vec![] 124 | /// selected: 0 125 | /// direction: Direction::Horizontal 126 | /// stretched: false 127 | /// style: "".to_string() 128 | /// listener: None 129 | /// ``` 130 | /// 131 | /// ## Style 132 | /// 133 | /// ```text 134 | /// div.tabs[.direction-vertical] 135 | /// div.tab-titles 136 | /// div.tab-title[.selected] 137 | /// div.tab 138 | /// ``` 139 | /// 140 | /// ## Example 141 | /// 142 | /// ``` 143 | /// use std::cell::RefCell; 144 | /// use std::rc::Rc; 145 | /// 146 | /// use neutrino::widgets::tabs::{Tabs, TabsListener, TabsState}; 147 | /// use neutrino::widgets::label::Label; 148 | /// use neutrino::utils::theme::Theme; 149 | /// use neutrino::{App, Window}; 150 | /// 151 | /// 152 | /// struct Dessert { 153 | /// index: u32, 154 | /// value: String, 155 | /// } 156 | /// 157 | /// impl Dessert { 158 | /// fn new() -> Self { 159 | /// Self { index: 0, value: "Cake".to_string() } 160 | /// } 161 | /// 162 | /// fn index(&self) -> u32 { 163 | /// self.index 164 | /// } 165 | /// 166 | /// fn value(&self) -> &str { 167 | /// &self.value 168 | /// } 169 | /// 170 | /// fn set(&mut self, index: u32, value: &str) { 171 | /// self.index = index; 172 | /// self.value = value.to_string(); 173 | /// } 174 | /// } 175 | /// 176 | /// 177 | /// struct MyTabsListener { 178 | /// dessert: Rc>, 179 | /// } 180 | /// 181 | /// impl MyTabsListener { 182 | /// pub fn new(dessert: Rc>) -> Self { 183 | /// Self { dessert } 184 | /// } 185 | /// } 186 | /// 187 | /// impl TabsListener for MyTabsListener { 188 | /// fn on_change(&self, state: &TabsState) { 189 | /// let index = state.selected(); 190 | /// self.dessert.borrow_mut().set( 191 | /// index, 192 | /// &state.titles()[index as usize] 193 | /// ); 194 | /// } 195 | /// 196 | /// fn on_update(&self, state: &mut TabsState) { 197 | /// state.set_selected(self.dessert.borrow().index()); 198 | /// } 199 | /// } 200 | /// 201 | /// 202 | /// fn main() { 203 | /// let dessert = Rc::new(RefCell::new(Dessert::new())); 204 | /// 205 | /// let my_listener = MyTabsListener::new(Rc::clone(&dessert)); 206 | /// 207 | /// let mut my_label = Label::new("my_label"); 208 | /// my_label.set_text("World!"); 209 | /// 210 | /// let mut my_tabs = Tabs::new("my_tabs"); 211 | /// my_tabs.add("Hello", Box::new(my_label)); 212 | /// my_tabs.set_listener(Box::new(my_listener)); 213 | /// } 214 | /// ``` 215 | pub struct Tabs { 216 | name: String, 217 | state: TabsState, 218 | listener: Option>, 219 | } 220 | 221 | impl Tabs { 222 | /// Create a Tabs 223 | pub fn new(name: &str) -> Self { 224 | Self { 225 | name: name.to_string(), 226 | state: TabsState { 227 | titles: vec![], 228 | children: vec![], 229 | selected: 0, 230 | direction: Direction::Horizontal, 231 | stretched: false, 232 | style: "".to_string(), 233 | }, 234 | listener: None, 235 | } 236 | } 237 | 238 | /// Set the selected index 239 | pub fn set_selected(&mut self, selected: u32) { 240 | self.state.set_selected(selected); 241 | } 242 | 243 | /// Set the direction 244 | /// 245 | /// # Example 246 | /// 247 | /// ```text 248 | /// Direction::Horizontal 249 | /// 250 | /// +-------+-------+ 251 | /// | Tab 1 | Tab 2 | 252 | /// +-------+-------+ 253 | /// | Content | 254 | /// | | 255 | /// +---------------+ 256 | /// 257 | /// Direction::Vertical 258 | /// 259 | /// +-------+-------------+ 260 | /// | Tab 1 | | 261 | /// +-------+ Content | 262 | /// | Tab 2 | | 263 | /// +-------+-------------+ 264 | /// ``` 265 | pub fn set_direction(&mut self, direction: Direction) { 266 | self.state.set_direction(direction); 267 | } 268 | 269 | /// Set the stretched flag to true 270 | pub fn set_stretched(&mut self) { 271 | self.state.set_stretched(true); 272 | } 273 | 274 | /// Set the listener 275 | pub fn set_listener(&mut self, listener: Box) { 276 | self.listener = Some(listener); 277 | } 278 | 279 | /// Set the style 280 | pub fn set_style(&mut self, style: &str) { 281 | self.state.set_style(style); 282 | } 283 | 284 | /// Add a tab 285 | pub fn add(&mut self, name: &str, child: Box) { 286 | self.state.add(name, child); 287 | } 288 | } 289 | 290 | impl Widget for Tabs { 291 | fn eval(&self) -> String { 292 | let stretched = if self.state.stretched() { 293 | "stretched" 294 | } else { 295 | "" 296 | }; 297 | let style = inline_style(&scss_to_css(&format!( 298 | r##"#{}{{{}}}"##, 299 | self.name, 300 | self.state.style(), 301 | ))); 302 | let mut html = format!( 303 | r#" 304 |
305 |
306 | "#, 307 | self.name, 308 | stretched, 309 | self.state.direction().css() 310 | ); 311 | let tabs_number = self.state.titles.len(); 312 | for (i, title) in self.state.titles.iter().enumerate() { 313 | let selected = if self.state.selected() == i as u32 { 314 | "selected" 315 | } else { 316 | "" 317 | }; 318 | let first = if i == 0 { "first" } else { "" }; 319 | let last = if i == tabs_number - 1 { "last" } else { "" }; 320 | html.push_str(&format!( 321 | r#" 322 |
323 | {} 324 |
325 | "#, 326 | first, 327 | last, 328 | selected, 329 | Event::change_js(&self.name, &format!("'{}'", i)), 330 | title 331 | )); 332 | } 333 | html.push_str(&format!( 334 | r#"
{}
"#, 335 | self.state.children[self.state.selected() as usize].eval() 336 | )); 337 | format!("{}{}", style, html) 338 | } 339 | 340 | fn trigger(&mut self, event: &Event) { 341 | match event { 342 | Event::Update => { 343 | self.state.children[self.state.selected as usize] 344 | .trigger(event); 345 | self.on_update() 346 | } 347 | Event::Change { source, value } => { 348 | if source == &self.name { 349 | self.on_change(value); 350 | } else { 351 | self.state.children[self.state.selected as usize] 352 | .trigger(event); 353 | }; 354 | } 355 | _ => { 356 | self.state.children[self.state.selected as usize].trigger( 357 | event 358 | ) 359 | } 360 | } 361 | } 362 | 363 | fn on_update(&mut self) { 364 | match &self.listener { 365 | None => (), 366 | Some(listener) => { 367 | listener.on_update(&mut self.state); 368 | } 369 | } 370 | } 371 | 372 | fn on_change(&mut self, value: &str) { 373 | let selected = value.parse::().unwrap(); 374 | if selected > -1 { 375 | self.state.set_selected(selected as u32); 376 | } 377 | match &self.listener { 378 | None => (), 379 | Some(listener) => { 380 | listener.on_change(&self.state); 381 | } 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/widgets/textinput.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | use crate::utils::style::{inline_style, scss_to_css}; 3 | use crate::widgets::widget::Widget; 4 | 5 | /// # The state of a TextInput 6 | /// 7 | /// ## Fields 8 | /// 9 | /// ```text 10 | /// value: String 11 | /// input_type: InputType, 12 | /// placeholder: String 13 | /// size: u32 14 | /// disabled: bool 15 | /// stretched: bool 16 | /// style: String 17 | /// ``` 18 | pub struct TextInputState { 19 | value: String, 20 | input_type: InputType, 21 | placeholder: String, 22 | size: u32, 23 | disabled: bool, 24 | stretched: bool, 25 | style: String, 26 | } 27 | 28 | impl TextInputState { 29 | /// Get the value 30 | pub fn value(&self) -> &str { 31 | &self.value 32 | } 33 | 34 | /// Get the input_type 35 | pub fn input_type(&self) -> &InputType { 36 | &self.input_type 37 | } 38 | 39 | /// Get the placeholder 40 | pub fn placeholder(&self) -> &str { 41 | &self.placeholder 42 | } 43 | 44 | /// Get the size 45 | pub fn size(&self) -> u32 { 46 | self.size 47 | } 48 | 49 | /// Get the disabled flag 50 | pub fn disabled(&self) -> bool { 51 | self.disabled 52 | } 53 | 54 | /// Get the stretched flag 55 | pub fn stretched(&self) -> bool { 56 | self.stretched 57 | } 58 | 59 | /// Get the style 60 | pub fn style(&self) -> &str { 61 | &self.style 62 | } 63 | 64 | /// Set the value 65 | pub fn set_value(&mut self, value: &str) { 66 | self.value = value.to_string(); 67 | } 68 | 69 | /// Set the input_type 70 | pub fn set_input_type(&mut self, input_type: InputType) { 71 | self.input_type = input_type; 72 | } 73 | 74 | /// Set the placeholder 75 | pub fn set_placeholder(&mut self, placeholder: &str) { 76 | self.placeholder = placeholder.to_string(); 77 | } 78 | 79 | /// Set the size 80 | pub fn set_size(&mut self, size: u32) { 81 | self.size = size; 82 | } 83 | 84 | /// Set the disabled flag 85 | pub fn set_disabled(&mut self, disabled: bool) { 86 | self.disabled = disabled; 87 | } 88 | 89 | /// Set the stretched flag 90 | pub fn set_stretched(&mut self, stretched: bool) { 91 | self.stretched = stretched; 92 | } 93 | 94 | /// Set the style 95 | pub fn set_style(&mut self, style: &str) { 96 | self.style = style.to_string(); 97 | } 98 | } 99 | 100 | /// # The listener of a TextInput 101 | pub trait TextInputListener { 102 | /// Function triggered on update event 103 | fn on_update(&self, state: &mut TextInputState); 104 | 105 | /// Function triggered on change event 106 | fn on_change(&self, state: &TextInputState); 107 | } 108 | 109 | /// # A zone where text can be written. 110 | /// 111 | /// ## Fields 112 | /// 113 | /// ```text 114 | /// name: String 115 | /// state: TextInputState 116 | /// listener: Option> 117 | /// ``` 118 | /// 119 | /// ## Default values 120 | /// 121 | /// ```text 122 | /// name: name.to_string() 123 | /// state: 124 | /// value: "".to_string() 125 | /// input_type: InputType::Text 126 | /// placeholder: "".to_string() 127 | /// size: 10 128 | /// disabled: false 129 | /// stretched: false 130 | /// style: "".to_string() 131 | /// listener: None 132 | /// ``` 133 | /// 134 | /// ## Style 135 | /// 136 | /// ```text 137 | /// div.textinput[.disabled] 138 | /// input[.focus] 139 | /// ``` 140 | /// 141 | /// 142 | /// ## Example 143 | /// 144 | /// ``` 145 | /// use std::cell::RefCell; 146 | /// use std::rc::Rc; 147 | /// 148 | /// use neutrino::widgets::textinput::{ 149 | /// TextInput, 150 | /// TextInputListener, 151 | /// TextInputState 152 | /// }; 153 | /// use neutrino::utils::theme::Theme; 154 | /// use neutrino::{App, Window}; 155 | /// 156 | /// 157 | /// struct Person { 158 | /// name: String, 159 | /// } 160 | /// 161 | /// impl Person { 162 | /// fn new() -> Self { 163 | /// Self { name: "Ferris".to_string() } 164 | /// } 165 | /// 166 | /// fn name(&self) -> &str { 167 | /// &self.name 168 | /// } 169 | /// 170 | /// fn set_name(&mut self, name: &str) { 171 | /// self.name = name.to_string(); 172 | /// } 173 | /// } 174 | /// 175 | /// 176 | /// struct MyTextInputListener { 177 | /// person: Rc>, 178 | /// } 179 | /// 180 | /// impl MyTextInputListener { 181 | /// pub fn new(person: Rc>) -> Self { 182 | /// Self { person } 183 | /// } 184 | /// } 185 | /// 186 | /// impl TextInputListener for MyTextInputListener { 187 | /// fn on_change(&self, state: &TextInputState) { 188 | /// self.person.borrow_mut().set_name(&state.value()); 189 | /// } 190 | /// 191 | /// fn on_update(&self, state: &mut TextInputState) { 192 | /// state.set_value(&self.person.borrow().name()); 193 | /// } 194 | /// } 195 | /// 196 | /// 197 | /// fn main() { 198 | /// let person = Rc::new(RefCell::new(Person::new())); 199 | /// 200 | /// let my_listener = MyTextInputListener::new(Rc::clone(&person)); 201 | /// 202 | /// let mut my_textinput = TextInput::new("my_textinput"); 203 | /// my_textinput.set_listener(Box::new(my_listener)); 204 | /// } 205 | /// ``` 206 | pub struct TextInput { 207 | name: String, 208 | state: TextInputState, 209 | listener: Option>, 210 | } 211 | 212 | impl TextInput { 213 | /// Create a TextInput 214 | pub fn new(name: &str) -> Self { 215 | Self { 216 | name: name.to_string(), 217 | state: TextInputState { 218 | value: "".to_string(), 219 | input_type: InputType::Text, 220 | placeholder: "".to_string(), 221 | size: 10, 222 | disabled: false, 223 | stretched: false, 224 | style: "".to_string(), 225 | }, 226 | listener: None, 227 | } 228 | } 229 | 230 | /// Set the value 231 | pub fn set_value(&mut self, value: &str) { 232 | self.state.set_value(value); 233 | } 234 | 235 | /// Set the input_type 236 | pub fn set_input_type(&mut self, input_type: InputType) { 237 | self.state.set_input_type(input_type); 238 | } 239 | 240 | /// Set the placeholder 241 | pub fn set_placeholder(&mut self, placeholder: &str) { 242 | self.state.set_placeholder(placeholder); 243 | } 244 | 245 | /// Set the size 246 | pub fn set_size(&mut self, size: u32) { 247 | self.state.set_size(size); 248 | } 249 | 250 | /// Set the stretched flag to true 251 | pub fn set_stretched(&mut self) { 252 | self.state.set_stretched(true); 253 | } 254 | 255 | /// Set the disabled flag to true 256 | pub fn set_disabled(&mut self) { 257 | self.state.set_disabled(true); 258 | } 259 | 260 | /// Set the listener 261 | pub fn set_listener(&mut self, listener: Box) { 262 | self.listener = Some(listener); 263 | } 264 | 265 | /// Set the style 266 | pub fn set_style(&mut self, style: &str) { 267 | self.state.set_style(style); 268 | } 269 | } 270 | 271 | impl Widget for TextInput { 272 | fn eval(&self) -> String { 273 | let stretched = if self.state.stretched() { 274 | "stretched" 275 | } else { 276 | "" 277 | }; 278 | let disabled = if self.state.disabled() { 279 | "disabled" 280 | } else { 281 | "" 282 | }; 283 | let style = inline_style(&scss_to_css(&format!( 284 | r##"#{}{{{}}}"##, 285 | self.name, 286 | self.state.style(), 287 | ))); 288 | let html = format!( 289 | r#" 290 |
291 | 293 |
294 | "#, 295 | self.name, 296 | disabled, 297 | stretched, 298 | disabled, 299 | self.state.input_type().css(), 300 | self.state.size(), 301 | self.state.size(), 302 | self.state.placeholder(), 303 | self.state.value(), 304 | Event::change_js(&self.name, "value"), 305 | Event::change_js(&self.name, "value"), 306 | ); 307 | format!("{}{}", style, html) 308 | } 309 | 310 | fn trigger(&mut self, event: &Event) { 311 | match event { 312 | Event::Update => self.on_update(), 313 | Event::Change { source, value } => { 314 | if source == &self.name && !self.state.disabled() { 315 | self.on_change(value); 316 | } 317 | } 318 | _ => (), 319 | } 320 | } 321 | 322 | fn on_update(&mut self) { 323 | match &self.listener { 324 | None => (), 325 | Some(listener) => { 326 | listener.on_update(&mut self.state); 327 | } 328 | } 329 | } 330 | 331 | fn on_change(&mut self, value: &str) { 332 | self.state.set_value(value); 333 | match &self.listener { 334 | None => (), 335 | Some(listener) => { 336 | listener.on_change(&self.state); 337 | } 338 | } 339 | } 340 | } 341 | 342 | pub enum InputType { 343 | Text, 344 | Password, 345 | } 346 | 347 | impl InputType { 348 | fn css(&self) -> &str { 349 | match &self { 350 | InputType::Text => "text", 351 | InputType::Password => "password", 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/widgets/widget.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::event::Event; 2 | 3 | /// # Trait that any of the widgets have to implement 4 | pub trait Widget { 5 | /// Return the HTML representation of the widget 6 | fn eval(&self) -> String; 7 | 8 | /// Trigger functions depending on the event 9 | fn trigger(&mut self, _event: &Event); 10 | 11 | /// Function triggered on update event 12 | fn on_update(&mut self); 13 | 14 | /// Function triggered on change event 15 | fn on_change(&mut self, _value: &str); 16 | } 17 | -------------------------------------------------------------------------------- /src/www/app/app.js: -------------------------------------------------------------------------------- 1 | let node = document.getElementById("app"); 2 | 3 | function render(template) { 4 | morphdom(node, template); 5 | } 6 | 7 | function emit(arg) { 8 | window.external.invoke(JSON.stringify(arg)); 9 | } 10 | 11 | window.onload = function() { 12 | emit({ type: "Update" }); 13 | } -------------------------------------------------------------------------------- /src/www/app/app.scss: -------------------------------------------------------------------------------- 1 | @mixin user-select($select) { 2 | -webkit-user-select: $select; 3 | -khtml-user-drag: $select; 4 | -khtml-user-select: $select; 5 | -moz-user-select: $select; 6 | @if $select == none { -moz-user-select: -moz-none; } 7 | -ms-user-select: $select; 8 | user-select: $select; 9 | } 10 | 11 | * { 12 | @include user-select(none); 13 | } 14 | 15 | input, textarea { 16 | @include user-select(text); 17 | } 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | #app { 24 | position: relative; 25 | height: 100vh; 26 | width: 100%; 27 | overflow: hidden; 28 | 29 | > * { 30 | position: absolute; 31 | top: 0; 32 | bottom: 0; 33 | left: 0; 34 | right: 0; 35 | } 36 | } 37 | 38 | .stretched { 39 | flex-grow: 1; 40 | } 41 | 42 | .container { 43 | display: flex; 44 | box-sizing: border-box; 45 | 46 | &.direction-horizontal { 47 | flex-direction: row; 48 | } 49 | 50 | &.direction-vertical { 51 | flex-direction: column; 52 | } 53 | 54 | &.position-start { 55 | justify-content: flex-start; 56 | } 57 | 58 | &.position-end { 59 | justify-content: flex-end; 60 | } 61 | 62 | &.position-center { 63 | justify-content: center; 64 | } 65 | 66 | &.position-between { 67 | justify-content: space-between; 68 | } 69 | 70 | &.position-around { 71 | justify-content: space-around; 72 | } 73 | 74 | &.alignment-center { 75 | align-items: center; 76 | } 77 | 78 | &.alignment-start { 79 | align-items: flex-start; 80 | } 81 | 82 | &.alignment-end { 83 | align-items: flex-end; 84 | } 85 | } 86 | 87 | .image { 88 | width: 100%; 89 | height: 100%; 90 | display: flex; 91 | flex-direction: column; 92 | align-items: center; 93 | justify-content: center; 94 | background: black; 95 | 96 | img { 97 | max-width: 100%; 98 | max-height: 100%; 99 | } 100 | } 101 | 102 | .button { 103 | white-space: nowrap; 104 | font-size: inherit; 105 | font-family: inherit; 106 | user-select: none; 107 | -webkit-user-select: none; 108 | cursor: default; 109 | display: flex; 110 | justify-content: center; 111 | align-items: center; 112 | } 113 | 114 | .progressbar { 115 | min-width: 100px; 116 | position: relative; 117 | 118 | .background { 119 | box-sizing: border-box; 120 | position: absolute; 121 | height: 100%; 122 | width: 100%; 123 | } 124 | 125 | .foreground { 126 | box-sizing: border-box; 127 | position: absolute; 128 | height: 100%; 129 | } 130 | } 131 | 132 | .checkbox { 133 | display: flex; 134 | align-items: center; 135 | 136 | label { 137 | white-space: nowrap; 138 | } 139 | 140 | .checkbox-outer { 141 | display: flex; 142 | justify-content: center; 143 | align-items: center; 144 | } 145 | } 146 | 147 | .radio { 148 | display: flex; 149 | align-items: center; 150 | 151 | label { 152 | white-space: nowrap; 153 | } 154 | 155 | .radio-outer { 156 | display: flex; 157 | justify-content: center; 158 | align-items: center; 159 | } 160 | } 161 | 162 | .combo { 163 | display: flex; 164 | flex-direction: column; 165 | position: relative; 166 | cursor: default; 167 | 168 | .combo-button { 169 | font-size: inherit; 170 | font-family: inherit; 171 | user-select: none; 172 | -webkit-user-select: none; 173 | display: flex; 174 | justify-content: space-between; 175 | align-items: center; 176 | position: relative; 177 | z-index: 11; 178 | } 179 | 180 | .combo-choices { 181 | position: absolute; 182 | z-index: 10; 183 | top: 100%; 184 | box-sizing: border-box; 185 | white-space: nowrap; 186 | min-width: 100%; 187 | } 188 | } 189 | 190 | .range { 191 | min-width: 100px; 192 | display: flex; 193 | align-items: center; 194 | 195 | .inner-range { 196 | padding: 0; 197 | box-sizing: border-box; 198 | outline: none; 199 | width: 100%; 200 | appearance: none; 201 | -webkit-appearance: none; 202 | 203 | &::-webkit-slider-runnable-track { 204 | -webkit-appearance: none; 205 | appearance: none; 206 | } 207 | 208 | &::-webkit-slider-thumb { 209 | cursor: pointer; 210 | -webkit-appearance: none; 211 | appearance: none; 212 | } 213 | 214 | &::-ms-thumb { 215 | border: none; 216 | } 217 | 218 | &::-ms-track { 219 | color: transparent; 220 | } 221 | 222 | &::-ms-fill-lower { 223 | background: transparent; 224 | } 225 | 226 | &::-moz-range-thumb { 227 | cursor: pointer; 228 | } 229 | } 230 | } 231 | 232 | .tabs { 233 | box-sizing: border-box; 234 | display: flex; 235 | flex-direction: column; 236 | 237 | &.direction-vertical { 238 | flex-direction: row; 239 | 240 | .tab-titles { 241 | flex-direction: column; 242 | width: 200px; 243 | } 244 | } 245 | 246 | .tab-titles { 247 | cursor: default; 248 | display: flex; 249 | z-index: 2; 250 | 251 | .tab-title { 252 | box-sizing: border-box; 253 | display: flex; 254 | align-items: center; 255 | } 256 | } 257 | 258 | .tab { 259 | flex-grow: 1; 260 | 261 | > * { 262 | width: 100%; 263 | height: 100%; 264 | } 265 | } 266 | } 267 | 268 | .menubar { 269 | display: flex; 270 | position: relative; 271 | width: 100%; 272 | cursor: default; 273 | box-sizing: border-box; 274 | 275 | .menuitem { 276 | height: 100%; 277 | 278 | .menuitem-title { 279 | height: 100%; 280 | display: flex; 281 | align-items: center; 282 | } 283 | 284 | .menufunctions { 285 | display: flex; 286 | flex-direction: column; 287 | position: absolute; 288 | top: 100%; 289 | z-index: 100; 290 | 291 | .menufunction { 292 | display: flex; 293 | justify-content: space-between; 294 | 295 | span { 296 | white-space: nowrap; 297 | } 298 | } 299 | } 300 | 301 | .underlined { 302 | text-decoration: underline; 303 | } 304 | } 305 | } 306 | 307 | 308 | .selectable { 309 | @include user-select(text); 310 | } 311 | 312 | .unselectable { 313 | @include user-select(none); 314 | } 315 | -------------------------------------------------------------------------------- /src/www/app/morphdom.min.js: -------------------------------------------------------------------------------- 1 | "use strict";function morphAttrs(e,t){var n,r,o,a,i,d=t.attributes;for(n=d.length-1;n>=0;--n)o=(r=d[n]).name,a=r.namespaceURI,i=r.value,a?(o=r.localName||o,e.getAttributeNS(a,o)!==i&&e.setAttributeNS(a,o,i)):e.getAttribute(o)!==i&&e.setAttribute(o,i);for(n=(d=e.attributes).length-1;n>=0;--n)!1!==(r=d[n]).specified&&(o=r.name,(a=r.namespaceURI)?(o=r.localName||o,t.hasAttributeNS(a,o)||e.removeAttributeNS(a,o)):t.hasAttribute(o)||e.removeAttribute(o))}var range,NS_XHTML="http://www.w3.org/1999/xhtml",doc="undefined"==typeof document?void 0:document,HAS_TEMPLATE_SUPPORT=!!doc&&"content"in doc.createElement("template"),HAS_RANGE_SUPPORT=!!doc&&doc.createRange&&"createContextualFragment"in doc.createRange();function createFragmentFromTemplate(e){var t=doc.createElement("template");return t.innerHTML=e,t.content.childNodes[0]}function createFragmentFromRange(e){return range||(range=doc.createRange()).selectNode(doc.body),range.createContextualFragment(e).childNodes[0]}function createFragmentFromWrap(e){var t=doc.createElement("body");return t.innerHTML=e,t.childNodes[0]}function toElement(e){return e=e.trim(),HAS_TEMPLATE_SUPPORT?createFragmentFromTemplate(e):HAS_RANGE_SUPPORT?createFragmentFromRange(e):createFragmentFromWrap(e)}function compareNodeNames(e,t){var n=e.nodeName,r=t.nodeName;return n===r||!!(t.actualize&&n.charCodeAt(0)<91&&r.charCodeAt(0)>90)&&n===r.toUpperCase()}function createElementNS(e,t){return t&&t!==NS_XHTML?doc.createElementNS(t,e):doc.createElement(e)}function moveChildren(e,t){for(var n=e.firstChild;n;){var r=n.nextSibling;t.appendChild(n),n=r}return t}function syncBooleanAttrProp(e,t,n){e[n]!==t[n]&&(e[n]=t[n],e[n]?e.setAttribute(n,""):e.removeAttribute(n))}var specialElHandlers={OPTION:function(e,t){var n=e.parentNode;if(n){var r=n.nodeName.toUpperCase();"OPTGROUP"===r&&(r=(n=n.parentNode)&&n.nodeName.toUpperCase()),"SELECT"!==r||n.hasAttribute("multiple")||(e.hasAttribute("selected")&&!t.selected&&(e.setAttribute("selected","selected"),e.removeAttribute("selected")),n.selectedIndex=-1)}syncBooleanAttrProp(e,t,"selected")},INPUT:function(e,t){syncBooleanAttrProp(e,t,"checked"),syncBooleanAttrProp(e,t,"disabled"),e.value!==t.value&&(e.value=t.value),t.hasAttribute("value")||e.removeAttribute("value")},TEXTAREA:function(e,t){var n=t.value;e.value!==n&&(e.value=n);var r=e.firstChild;if(r){var o=r.nodeValue;if(o==n||!n&&o==e.placeholder)return;r.nodeValue=n}},SELECT:function(e,t){if(!t.hasAttribute("multiple")){for(var n,r,o=-1,a=0,i=e.firstChild;i;)if("OPTGROUP"===(r=i.nodeName&&i.nodeName.toUpperCase()))i=(n=i).firstChild;else{if("OPTION"===r){if(i.hasAttribute("selected")){o=a;break}a++}!(i=i.nextSibling)&&n&&(i=n.nextSibling,n=null)}e.selectedIndex=o}}},ELEMENT_NODE=1,DOCUMENT_FRAGMENT_NODE=11,TEXT_NODE=3,COMMENT_NODE=8;function noop(){}function defaultGetNodeKey(e){return e.id}function morphdomFactory(e){return function(t,n,r){if(r||(r={}),"string"==typeof n)if("#document"===t.nodeName||"HTML"===t.nodeName){var o=n;(n=doc.createElement("html")).innerHTML=o}else n=toElement(n);var a,i=r.getNodeKey||defaultGetNodeKey,d=r.onBeforeNodeAdded||noop,l=r.onNodeAdded||noop,c=r.onBeforeElUpdated||noop,u=r.onElUpdated||noop,m=r.onBeforeNodeDiscarded||noop,N=r.onNodeDiscarded||noop,f=r.onBeforeElChildrenUpdated||noop,s=!0===r.childrenOnly,p={};function E(e){a?a.push(e):a=[e]}function T(e,t,n){!1!==m(e)&&(t&&t.removeChild(e),N(e),function e(t,n){if(t.nodeType===ELEMENT_NODE)for(var r=t.firstChild;r;){var o=void 0;n&&(o=i(r))?E(o):(N(r),r.firstChild&&e(r,n)),r=r.nextSibling}}(e,n))}function v(e){l(e);for(var t=e.firstChild;t;){var n=t.nextSibling,r=i(t);if(r){var o=p[r];o&&compareNodeNames(t,o)&&(t.parentNode.replaceChild(o,t),h(o,t))}v(t),t=n}}function h(r,o,a){var l=i(o);if(l&&delete p[l],!n.isSameNode||!n.isSameNode(t)){if(!a){if(!1===c(r,o))return;if(e(r,o),u(r),!1===f(r,o))return}"TEXTAREA"!==r.nodeName?function(e,t){var n,r,o,a,l,c=t.firstChild,u=e.firstChild;e:for(;c;){for(a=c.nextSibling,n=i(c);u;){if(o=u.nextSibling,c.isSameNode&&c.isSameNode(u)){c=a,u=o;continue e}r=i(u);var m=u.nodeType,N=void 0;if(m===c.nodeType&&(m===ELEMENT_NODE?(n?n!==r&&((l=p[n])?o===l?N=!1:(e.insertBefore(l,u),r?E(r):T(u,e,!0),u=l):N=!1):r&&(N=!1),(N=!1!==N&&compareNodeNames(u,c))&&h(u,c)):m!==TEXT_NODE&&m!=COMMENT_NODE||(N=!0,u.nodeValue!==c.nodeValue&&(u.nodeValue=c.nodeValue))),N){c=a,u=o;continue e}r?E(r):T(u,e,!0),u=o}if(n&&(l=p[n])&&compareNodeNames(l,c))e.appendChild(l),h(l,c);else{var f=d(c);!1!==f&&(f&&(c=f),c.actualize&&(c=c.actualize(e.ownerDocument||doc)),e.appendChild(c),v(c))}c=a,u=o}!function(e,t,n){for(;t;){var r=t.nextSibling;(n=i(t))?E(n):T(t,e,!0),t=r}}(e,u,r);var s=specialElHandlers[e.nodeName];s&&s(e,t)}(r,o):specialElHandlers.TEXTAREA(r,o)}}!function e(t){if(t.nodeType===ELEMENT_NODE||t.nodeType===DOCUMENT_FRAGMENT_NODE)for(var n=t.firstChild;n;){var r=i(n);r&&(p[r]=n),e(n),n=n.nextSibling}}(t);var A=t,g=A.nodeType,C=n.nodeType;if(!s)if(g===ELEMENT_NODE)C===ELEMENT_NODE?compareNodeNames(t,n)||(N(t),A=moveChildren(t,createElementNS(n.nodeName,n.namespaceURI))):A=n;else if(g===TEXT_NODE||g===COMMENT_NODE){if(C===g)return A.nodeValue!==n.nodeValue&&(A.nodeValue=n.nodeValue),A;A=n}if(A===n)N(t);else if(h(A,n,s),a)for(var b=0,O=a.length;b 2 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Down.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Left.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Right.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Unlock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/Up.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/ZoomIn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Breeze/ZoomOut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/icons/Default/Down.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/www/icons/Default/Plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/www/themes/Adwaita.scss: -------------------------------------------------------------------------------- 1 | // MENUBAR HEIGHT 2 | $menubar-height: 26px; 3 | 4 | // COLORS 5 | $primary-color: #3584e4; 6 | $dprimary-color: #185fb4; 7 | $dgrey-color: #757778; 8 | $mgrey-color: #b5b5b6; 9 | $lgrey-color: #e1dedb; 10 | $background-color: #f6f5f4; 11 | $disabled-color: #faf9f8; 12 | 13 | // GRADIENTS 14 | $button-gradient: linear-gradient(white 0, #f6f5f3 1px, #f1efee 50%, #edebe9 100%); 15 | $disabled-gradient: linear-gradient($lgrey-color, $lgrey-color); 16 | 17 | // MARGINS 18 | $widget-margin: 6px; 19 | 20 | // PATH 21 | // Base 64 encoded string from : 22 | // 23 | $checkbox-path: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PScwIDAgMTAwIDEwMCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cG9seWdvbiBwb2ludHM9JzUwIDU1LCAxMDAgMTAsIDEwMCA0MCwgNTAgODUsIDE0IDQzLCAyOSAyOScgZmlsbD0nd2hpdGUnPjwvcG9seWdvbj48L3N2Zz4="); 24 | 25 | // Base 64 encoded string from : 26 | // 27 | $disabled-checkbox-path: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PScwIDAgMTAwIDEwMCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cG9seWdvbiBwb2ludHM9JzUwIDU1LCAxMDAgMTAsIDEwMCA0MCwgNTAgODUsIDE0IDQzLCAyOSAyOScgZmlsbD0nYmxhY2snPjwvcG9seWdvbj48L3N2Zz4="); 28 | 29 | // Base 64 encoded string from : 30 | // 31 | $combo-path: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PScwIDAgMTAwIDEwMCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cG9seWdvbiBwb2ludHM9JzUwIDYwLCAyOCA0MCwgNzIgNDAnIGZpbGw9J2JsYWNrJz48L3BvbHlnb24+PC9zdmc+"); 32 | 33 | 34 | #app { 35 | font-family: 'Cantarell', sans-serif; 36 | font-size: 14px; 37 | background-color: $background-color; 38 | } 39 | 40 | .label { 41 | margin: $widget-margin; 42 | font-size: inherit; 43 | font-family: inherit; 44 | } 45 | 46 | .button { 47 | padding: 7px; 48 | margin: $widget-margin; 49 | border: 1px solid $mgrey-color; 50 | border-radius: 4px; 51 | background: $button-gradient; 52 | color: black; 53 | outline: 0; 54 | 55 | &.disabled { 56 | background: $disabled-color; 57 | color: lighten(black, 50%); 58 | } 59 | 60 | img { 61 | height: 18px; 62 | width: 18px; 63 | 64 | + span { 65 | margin-left: 10px; 66 | } 67 | 68 | } 69 | } 70 | 71 | .progressbar { 72 | margin-top: 14px; 73 | margin-bottom: 14px; 74 | margin-left: $widget-margin; 75 | margin-right: $widget-margin; 76 | height: 4px; 77 | 78 | .background { 79 | background-color: $lgrey-color; 80 | border: 1px solid $mgrey-color; 81 | border-radius: 2px; 82 | } 83 | 84 | .foreground { 85 | background-color: $primary-color; 86 | border: 1px solid $dprimary-color; 87 | border-radius: 2px; 88 | min-width: 4px; 89 | } 90 | } 91 | 92 | .textinput { 93 | margin: $widget-margin; 94 | 95 | input { 96 | border: 1px solid $mgrey-color; 97 | border-radius: 4px; 98 | margin: 0; 99 | padding: 7px; 100 | font-size: inherit; 101 | font-family: inherit; 102 | outline: 0; 103 | 104 | &:focus { 105 | border-color: $primary-color; 106 | } 107 | } 108 | 109 | &.disabled { 110 | input { 111 | background: $disabled-color; 112 | color: lighten(black, 20%); 113 | } 114 | } 115 | } 116 | 117 | .checkbox { 118 | margin: $widget-margin; 119 | 120 | label { 121 | margin-left: $widget-margin; 122 | } 123 | 124 | .checkbox-outer { 125 | height: 14px; 126 | width: 14px; 127 | background-color: white; 128 | border: 1px solid $mgrey-color; 129 | border-radius: 2px; 130 | 131 | .checkbox-inner { 132 | width: 14px; 133 | height: 14px; 134 | background-image: $checkbox-path; 135 | background-size: 14px 14px; 136 | visibility: hidden; 137 | } 138 | } 139 | 140 | &.checked { 141 | .checkbox-outer { 142 | background: $primary-color; 143 | border-color: $dprimary-color; 144 | 145 | .checkbox-inner { 146 | visibility: visible; 147 | } 148 | } 149 | } 150 | 151 | &.disabled { 152 | .checkbox-outer { 153 | background: $disabled-color; 154 | } 155 | 156 | label { 157 | color: lighten(black, 50%); 158 | } 159 | 160 | &.checked { 161 | .checkbox-outer { 162 | border-color: $mgrey-color; 163 | 164 | .checkbox-inner { 165 | background-image: $disabled-checkbox-path; 166 | opacity: 0.4; 167 | } 168 | } 169 | } 170 | } 171 | } 172 | 173 | .radio { 174 | margin: $widget-margin; 175 | 176 | label { 177 | margin-left: $widget-margin; 178 | } 179 | 180 | .radio-outer { 181 | height: 14px; 182 | width: 14px; 183 | background-color: white; 184 | border: 1px solid $mgrey-color; 185 | border-radius: 50%; 186 | 187 | .radio-inner { 188 | height: 6px; 189 | width: 6px; 190 | border-radius: 50%; 191 | background-color: white; 192 | visibility: hidden; 193 | } 194 | } 195 | 196 | &.selected { 197 | .radio-outer { 198 | background: $primary-color; 199 | border-color: $dprimary-color; 200 | 201 | .radio-inner { 202 | visibility: visible; 203 | } 204 | } 205 | } 206 | 207 | &.disabled { 208 | .radio-outer { 209 | background: $disabled-color; 210 | } 211 | 212 | label { 213 | color: lighten(black, 50%); 214 | } 215 | 216 | &.selected { 217 | .radio-outer { 218 | border-color: $mgrey-color; 219 | 220 | .radio-inner { 221 | background: black; 222 | opacity: 0.4; 223 | } 224 | } 225 | } 226 | } 227 | } 228 | 229 | .combo { 230 | margin: $widget-margin; 231 | 232 | .combo-button { 233 | padding: 7px; 234 | border: 1px solid $mgrey-color; 235 | border-radius: 4px; 236 | color: black; 237 | outline: 0; 238 | background: $button-gradient; 239 | 240 | .combo-icon { 241 | margin-left: 10px; 242 | height: 18px; 243 | width: 18px; 244 | background-image: $combo-path; 245 | background-size: 18px 18px; 246 | } 247 | } 248 | 249 | .combo-choices { 250 | box-shadow: 0 0 2px lighten(black, 50%); 251 | 252 | .combo-choice { 253 | background-color: white; 254 | padding: 6px; 255 | 256 | &:hover { 257 | background-color: $primary-color; 258 | color: white; 259 | } 260 | } 261 | } 262 | 263 | &.opened { 264 | .combo-button { 265 | border-bottom-left-radius: 0; 266 | border-bottom-right-radius: 0; 267 | } 268 | } 269 | 270 | &.disabled { 271 | .combo-button { 272 | background: $disabled-color; 273 | color: lighten(black, 50%); 274 | 275 | .combo-icon { 276 | opacity: 0.4; 277 | } 278 | } 279 | } 280 | } 281 | 282 | .range { 283 | margin-left: $widget-margin; 284 | margin-right: $widget-margin; 285 | height: 32px; 286 | 287 | .inner-range { 288 | background-color: white; 289 | 290 | &::-webkit-slider-runnable-track { 291 | height: 4px; 292 | background-color: $lgrey-color; 293 | border: 1px solid $mgrey-color; 294 | border-radius: 2px; 295 | } 296 | 297 | &::-webkit-slider-thumb { 298 | width: 18px; 299 | height: 18px; 300 | background: $button-gradient; 301 | border: 1px solid $mgrey-color; 302 | border-radius: 50%; 303 | margin-top: -8px; 304 | } 305 | 306 | &::-ms-track { 307 | margin-top: 10px; 308 | margin-bottom: 10px; 309 | height: 2px; 310 | background-color: $lgrey-color; 311 | border: 1px solid $mgrey-color; 312 | border-radius: 2px; 313 | } 314 | 315 | &::-ms-thumb { 316 | width: 16px; 317 | height: 16px; 318 | background: $button-gradient; 319 | border: 1px solid $mgrey-color; 320 | border-radius: 50%; 321 | margin-top: -2px; 322 | } 323 | } 324 | 325 | &.disabled { 326 | .inner-range { 327 | &::-webkit-slider-runnable-track { 328 | background: $disabled-color; 329 | } 330 | 331 | &::-webkit-slider-thumb { 332 | background: $disabled-color; 333 | } 334 | 335 | &::-ms-track { 336 | background: $disabled-color; 337 | } 338 | 339 | &::-ms-thumb { 340 | background: $disabled-color; 341 | } 342 | } 343 | } 344 | } 345 | 346 | .tabs { 347 | padding: $widget-margin; 348 | 349 | &.direction-vertical { 350 | padding: 0; 351 | 352 | .tab-titles { 353 | background-color: white; 354 | border: none; 355 | font-weight: normal; 356 | 357 | .tab-title { 358 | color: black; 359 | margin-bottom: 0; 360 | border: none; 361 | border-bottom: 1px solid $lgrey-color; 362 | 363 | &.selected { 364 | border: none; 365 | border-bottom: 1px solid $lgrey-color; 366 | background-color: $primary-color; 367 | color: white; 368 | } 369 | } 370 | } 371 | 372 | .tab { 373 | border: none; 374 | border-left: 1px solid $lgrey-color; 375 | } 376 | } 377 | 378 | .tab-titles { 379 | background-color: $lgrey-color; 380 | border: 1px solid $mgrey-color; 381 | border-bottom: none; 382 | font-weight: bold; 383 | 384 | .tab-title { 385 | margin-bottom: -1px; 386 | color: lighten(black, 60%); 387 | height: 36px; 388 | padding-left: 13px; 389 | padding-right: 13px; 390 | border-bottom: 1px solid $mgrey-color; 391 | border-top: 1px solid $lgrey-color; 392 | 393 | &.selected { 394 | color: lighten(black, 10%); 395 | border-bottom: 3px solid $primary-color; 396 | border-top: 3px solid $lgrey-color; 397 | } 398 | } 399 | } 400 | 401 | .tab { 402 | border: 1px solid $mgrey-color; 403 | border-radius: 2px; 404 | border-top-left-radius: 0; 405 | border-top-right-radius: 0; 406 | background-color: white; 407 | } 408 | } 409 | 410 | #app { 411 | .menubar ~ * { 412 | top: $menubar-height; 413 | } 414 | } 415 | 416 | .menubar { 417 | height: $menubar-height; 418 | background-color: $background-color; 419 | border-bottom: 1px solid $lgrey-color; 420 | 421 | .menuitem { 422 | 423 | .menuitem-title { 424 | box-sizing: border-box; 425 | padding-left: 11px; 426 | padding-right: 11px; 427 | 428 | &.selected { 429 | color: $dprimary-color; 430 | border-top: 3px solid $background-color; 431 | border-bottom: 3px solid $primary-color; 432 | } 433 | } 434 | 435 | .menufunctions { 436 | background-color: white; 437 | box-shadow: 0 0 2px lighten(black, 50%); 438 | 439 | .menufunction { 440 | padding-top: 6px; 441 | padding-bottom: 6px; 442 | padding-left: 11px; 443 | padding-right: 11px; 444 | width: 140px; 445 | 446 | .shortcut { 447 | color: $dgrey-color; 448 | } 449 | 450 | &:hover { 451 | background-color: $primary-color; 452 | color: white; 453 | 454 | .shortcut { 455 | color: white; 456 | } 457 | } 458 | 459 | 460 | } 461 | } 462 | } 463 | } -------------------------------------------------------------------------------- /src/www/themes/Breeze.scss: -------------------------------------------------------------------------------- 1 | // MENUBAR HEIGHT 2 | $menubar-height: 30px; 3 | 4 | // COLORS 5 | $primary-color: #3daee9; 6 | $dgrey-color: #757778; 7 | $mgrey-color: #b5b5b6; 8 | $lgrey-color: #d0d2d2; 9 | $background-color: #f3f4f4; 10 | $disabled-color: #e3e4e6; 11 | 12 | // GRADIENTS 13 | $button-gradient: linear-gradient($background-color, #e8e9ea); 14 | 15 | // MARGINS 16 | $widget-margin: 6px; 17 | 18 | // PATH 19 | // Base 64 encoded string from : 20 | // 21 | $combo-path: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PScwIDAgMTAwIDEwMCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cG9seWdvbiBwb2ludHM9JzUwIDYwLCA4MCAzNSwgODAgNDUsIDUwIDcwLCAyMCA0NSwgMjAgMzUnIGZpbGw9J2JsYWNrJz48L3BvbHlnb24+PC9zdmc+"); 22 | 23 | 24 | #app { 25 | font-family: 'Noto Sans', sans-serif; 26 | font-size: 13px; 27 | background-color: $background-color; 28 | } 29 | 30 | .label { 31 | margin: $widget-margin; 32 | font-size: inherit; 33 | font-family: inherit; 34 | } 35 | 36 | .button { 37 | padding: 6px; 38 | margin: $widget-margin; 39 | border: 1px solid $mgrey-color; 40 | border-radius: 2px; 41 | background: $button-gradient; 42 | color: black; 43 | outline: 0; 44 | 45 | &.disabled { 46 | background: $disabled-color; 47 | color: lighten(black, 50%); 48 | } 49 | 50 | img { 51 | height: 18px; 52 | width: 18px; 53 | 54 | + span { 55 | margin-left: 10px; 56 | } 57 | 58 | } 59 | } 60 | 61 | .progressbar { 62 | margin-top: 14px; 63 | margin-bottom: 14px; 64 | margin-left: $widget-margin; 65 | margin-right: $widget-margin; 66 | height: 6px; 67 | 68 | .background { 69 | background-color: $mgrey-color; 70 | border-radius: 3px; 71 | } 72 | 73 | .foreground { 74 | background-color: $primary-color; 75 | border-radius: 3px; 76 | min-width: 6px; 77 | } 78 | } 79 | 80 | .textinput { 81 | margin: $widget-margin; 82 | 83 | input { 84 | border: 1px solid $mgrey-color; 85 | border-radius: 2px; 86 | margin: 0; 87 | padding: 6px; 88 | font-size: inherit; 89 | font-family: inherit; 90 | outline: 0; 91 | 92 | &:focus { 93 | border-color: $primary-color; 94 | } 95 | } 96 | 97 | &.disabled { 98 | input { 99 | background: $disabled-color; 100 | color: lighten(black, 50%); 101 | } 102 | } 103 | } 104 | 105 | .checkbox { 106 | margin: $widget-margin; 107 | 108 | label { 109 | margin-left: $widget-margin; 110 | } 111 | 112 | .checkbox-outer { 113 | height: 14px; 114 | width: 14px; 115 | background-color: white; 116 | border: 1px solid $dgrey-color; 117 | border-radius: 2px; 118 | 119 | .checkbox-inner { 120 | height: 10px; 121 | width: 10px; 122 | background-color: white; 123 | visibility: hidden; 124 | } 125 | } 126 | 127 | &.checked { 128 | .checkbox-outer { 129 | border: 1px solid $primary-color; 130 | 131 | .checkbox-inner { 132 | background-color: $primary-color; 133 | visibility: visible; 134 | } 135 | } 136 | } 137 | 138 | &.disabled { 139 | .checkbox-outer { 140 | background: $disabled-color; 141 | border-color: $mgrey-color; 142 | } 143 | 144 | label { 145 | color: lighten(black, 50%); 146 | } 147 | 148 | &.checked { 149 | .checkbox-outer { 150 | border-color: $mgrey-color; 151 | 152 | .checkbox-inner { 153 | background-color: $mgrey-color; 154 | } 155 | } 156 | } 157 | } 158 | } 159 | 160 | .radio { 161 | margin: $widget-margin; 162 | 163 | label { 164 | margin-left: $widget-margin; 165 | } 166 | 167 | .radio-outer { 168 | height: 14px; 169 | width: 14px; 170 | background-color: white; 171 | border: 1px solid $dgrey-color; 172 | border-radius: 50%; 173 | 174 | .radio-inner { 175 | height: 10px; 176 | width: 10px; 177 | border-radius: 50%; 178 | background-color: white; 179 | visibility: hidden; 180 | } 181 | } 182 | 183 | &.selected { 184 | .radio-outer { 185 | border: 1px solid $primary-color; 186 | 187 | .radio-inner { 188 | background-color: $primary-color; 189 | visibility: visible; 190 | } 191 | } 192 | } 193 | 194 | &.disabled { 195 | .radio-outer { 196 | background: $disabled-color; 197 | border-color: $mgrey-color; 198 | } 199 | 200 | label { 201 | color: lighten(black, 50%); 202 | } 203 | 204 | &.selected { 205 | .radio-outer { 206 | border-color: $mgrey-color; 207 | 208 | .radio-inner { 209 | background-color: $mgrey-color; 210 | } 211 | } 212 | } 213 | } 214 | } 215 | 216 | .combo { 217 | margin: $widget-margin; 218 | 219 | .combo-button { 220 | padding: 6px; 221 | border: 1px solid $mgrey-color; 222 | border-radius: 2px; 223 | color: black; 224 | outline: 0; 225 | background: $button-gradient; 226 | 227 | .combo-icon { 228 | margin-left: 10px; 229 | height: 18px; 230 | width: 18px; 231 | background-image: $combo-path; 232 | background-size: 18px 18px; 233 | } 234 | } 235 | 236 | .combo-choices { 237 | border: 1px solid $mgrey-color; 238 | border-top: 0; 239 | box-shadow: 0 0 10px lighten(black, 75%);; 240 | 241 | .combo-choice { 242 | background-color: white; 243 | padding: 6px; 244 | 245 | &:hover { 246 | background-color: $primary-color; 247 | color: white; 248 | } 249 | } 250 | } 251 | 252 | &.opened { 253 | .combo-button { 254 | border-bottom-left-radius: 0; 255 | border-bottom-right-radius: 0; 256 | } 257 | } 258 | 259 | &.disabled { 260 | .combo-button { 261 | background: $disabled-color; 262 | color: lighten(black, 50%); 263 | 264 | .combo-icon { 265 | opacity: 0.4; 266 | } 267 | } 268 | } 269 | } 270 | 271 | .range { 272 | margin: $widget-margin; 273 | 274 | .inner-range { 275 | background-color: $background-color; 276 | 277 | &::-webkit-slider-runnable-track { 278 | height: 6px; 279 | background-color: $mgrey-color; 280 | border: 1px solid $mgrey-color; 281 | border-radius: 3px; 282 | } 283 | 284 | &::-webkit-slider-thumb { 285 | width: 18px; 286 | height: 18px; 287 | background: $button-gradient; 288 | border: 1px solid $dgrey-color; 289 | border-radius: 50%; 290 | margin-top: -7px; 291 | } 292 | 293 | &::-ms-track { 294 | margin-top: 10px; 295 | margin-bottom: 10px; 296 | height: 4px; 297 | background-color: $mgrey-color; 298 | border: 1px solid $mgrey-color; 299 | border-radius: 3px; 300 | } 301 | 302 | &::-ms-thumb { 303 | width: 16px; 304 | height: 16px; 305 | background: $button-gradient; 306 | border: 1px solid $dgrey-color; 307 | border-radius: 50%; 308 | margin-top: -1px; 309 | } 310 | } 311 | 312 | &.disabled { 313 | .inner-range { 314 | &::-webkit-slider-thumb { 315 | background: $disabled-color; 316 | border-color: $mgrey-color; 317 | } 318 | 319 | &::-ms-thumb { 320 | background: $disabled-color; 321 | border-color: $mgrey-color; 322 | } 323 | } 324 | } 325 | } 326 | 327 | .tabs { 328 | padding: $widget-margin; 329 | 330 | &.direction-vertical { 331 | padding: 0; 332 | 333 | .tab-titles { 334 | background-color: white; 335 | border: none; 336 | 337 | .tab-title { 338 | background-color: white; 339 | color: black; 340 | margin-bottom: 0; 341 | border: none; 342 | 343 | &.selected { 344 | border: none; 345 | border-bottom: 1px solid $lgrey-color; 346 | background-color: $primary-color; 347 | color: white; 348 | } 349 | } 350 | } 351 | 352 | .tab { 353 | border: none; 354 | border-left: 1px solid $lgrey-color; 355 | } 356 | } 357 | 358 | .tab-titles { 359 | margin-bottom: -1px; 360 | 361 | .tab-title { 362 | color: black; 363 | background-color: $lgrey-color; 364 | height: 28px; 365 | padding-left: 13px; 366 | padding-right: 13px; 367 | border: 1px solid transparent; 368 | border-bottom-color: $mgrey-color; 369 | 370 | &.selected { 371 | color: black; 372 | background-color: $background-color; 373 | border: 1px solid $mgrey-color; 374 | border-bottom-color: $background-color; 375 | } 376 | 377 | &:not(.selected) + .tab-title:not(.selected) { 378 | border-left-color: $mgrey-color; 379 | } 380 | } 381 | } 382 | 383 | .tab { 384 | border: 1px solid $mgrey-color; 385 | border-radius: 2px; 386 | border-top-left-radius: 0; 387 | background-color: $background-color; 388 | } 389 | } 390 | 391 | #app { 392 | .menubar ~ * { 393 | top: $menubar-height; 394 | } 395 | } 396 | 397 | .menubar { 398 | height: $menubar-height;; 399 | background-color: $background-color; 400 | 401 | .menuitem { 402 | 403 | .menuitem-title { 404 | padding-left: 11px; 405 | padding-right: 11px; 406 | 407 | &.selected { 408 | color: white; 409 | background-color: $primary-color; 410 | } 411 | } 412 | 413 | .menufunctions { 414 | background-color: white; 415 | border: 1px solid $mgrey-color; 416 | box-shadow: 0 0 10px lighten(black, 75%); 417 | 418 | .menufunction { 419 | padding-top: 6px; 420 | padding-bottom: 6px; 421 | padding-left: 11px; 422 | padding-right: 11px; 423 | width: 140px; 424 | 425 | &:hover { 426 | background-color: $primary-color; 427 | color: white; 428 | } 429 | } 430 | } 431 | } 432 | } -------------------------------------------------------------------------------- /src/www/themes/Default.scss: -------------------------------------------------------------------------------- 1 | // MENUBAR HEIGHT 2 | $menubar-height: 30px; 3 | 4 | // PATH 5 | // Base 64 encoded string from : 6 | // 7 | $combo-path: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PScwIDAgMTAwIDEwMCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cG9seWdvbiBwb2ludHM9JzUwIDYwLCA4MCAzNSwgODAgNDUsIDUwIDcwLCAyMCA0NSwgMjAgMzUnIGZpbGw9J2JsYWNrJz48L3BvbHlnb24+PC9zdmc+"); 8 | 9 | 10 | #app { 11 | font-family: sans-serif; 12 | font-size: 13px; 13 | background-color: white; 14 | } 15 | 16 | .label { 17 | margin: 6px; 18 | font-size: inherit; 19 | font-family: inherit; 20 | } 21 | 22 | .button { 23 | margin: 6px; 24 | border: 1px solid black; 25 | background: white; 26 | color: black; 27 | outline: 0; 28 | padding: 6px; 29 | 30 | img { 31 | height: 18px; 32 | width: 18px; 33 | 34 | + span { 35 | margin-left: 10px; 36 | } 37 | 38 | } 39 | } 40 | 41 | .progressbar { 42 | margin-top: 8px; 43 | margin-bottom: 8px; 44 | margin-left: 6px; 45 | margin-right: 6px; 46 | height: 10px; 47 | 48 | .background { 49 | background-color: white; 50 | border: 1px solid black; 51 | } 52 | 53 | .foreground { 54 | background-color: black; 55 | } 56 | } 57 | 58 | .textinput { 59 | margin: 6px; 60 | 61 | input { 62 | border: 1px solid black; 63 | background: white; 64 | margin: 0; 65 | padding: 6px; 66 | font-size: inherit; 67 | font-family: inherit; 68 | outline: 0; 69 | } 70 | } 71 | 72 | .checkbox { 73 | margin: 6px; 74 | 75 | label { 76 | margin-left: 6px; 77 | } 78 | 79 | .checkbox-outer { 80 | height: 14px; 81 | width: 14px; 82 | background-color: white; 83 | border: 1px solid black; 84 | 85 | .checkbox-inner { 86 | height: 10px; 87 | width: 10px; 88 | background-color: white; 89 | } 90 | } 91 | 92 | &.checked { 93 | .checkbox-outer { 94 | .checkbox-inner { 95 | background-color: black; 96 | } 97 | } 98 | } 99 | } 100 | 101 | .radio { 102 | margin: 6px; 103 | 104 | label { 105 | margin-left: 6px; 106 | } 107 | 108 | .radio-outer { 109 | height: 14px; 110 | width: 14px; 111 | background-color: white; 112 | border: 1px solid black; 113 | border-radius: 50%; 114 | 115 | .radio-inner { 116 | height: 10px; 117 | width: 10px; 118 | border-radius: 50%; 119 | background-color: black; 120 | opacity: 0; 121 | } 122 | } 123 | 124 | &.selected { 125 | .radio-outer { 126 | .radio-inner { 127 | opacity: 1; 128 | } 129 | } 130 | } 131 | } 132 | 133 | .combo { 134 | margin: 6px; 135 | 136 | .combo-button { 137 | border: 1px solid black; 138 | background: white; 139 | color: black; 140 | outline: 0; 141 | padding: 6px; 142 | 143 | .combo-icon { 144 | margin-left: 10px; 145 | height: 18px; 146 | width: 18px; 147 | background-image: $combo-path; 148 | background-size: 18px 18px; 149 | } 150 | } 151 | 152 | .combo-choices { 153 | border: 1px solid black; 154 | border-top: 0; 155 | 156 | .combo-choice { 157 | background: white; 158 | padding: 6px; 159 | 160 | &:hover { 161 | background-color: black; 162 | color: white; 163 | } 164 | } 165 | } 166 | } 167 | 168 | .range { 169 | margin: 6px; 170 | 171 | .inner-range { 172 | 173 | &::-webkit-slider-runnable-track { 174 | height: 10px; 175 | background-color: white; 176 | border: 1px solid black; 177 | } 178 | 179 | &::-webkit-slider-thumb { 180 | width: 16px; 181 | height: 8px; 182 | background: black; 183 | } 184 | 185 | &::-ms-track { 186 | height: 8px; 187 | background-color: white; 188 | border: 1px solid black; 189 | } 190 | 191 | &::-ms-thumb { 192 | width: 16px; 193 | height: 8px; 194 | background: black; 195 | } 196 | } 197 | } 198 | 199 | .tabs { 200 | padding: 6px; 201 | 202 | &.direction-vertical { 203 | padding: 0; 204 | 205 | .tab-titles { 206 | background-color: black; 207 | 208 | .tab-title { 209 | border: none; 210 | } 211 | } 212 | 213 | .tab { 214 | border: none; 215 | border-left: 1px solid black; 216 | } 217 | } 218 | 219 | .tab-titles { 220 | 221 | .tab-title { 222 | color: white; 223 | background-color: black; 224 | height: 28px; 225 | padding-left: 13px; 226 | padding-right: 13px; 227 | border: 1px solid black; 228 | 229 | &.selected { 230 | color: black; 231 | background-color: white; 232 | border-bottom-color: white; 233 | } 234 | } 235 | } 236 | 237 | .tab { 238 | border: 1px solid black; 239 | background: white; 240 | } 241 | } 242 | 243 | #app { 244 | .menubar ~ * { 245 | top: $menubar-height; 246 | } 247 | } 248 | 249 | .menubar { 250 | height: $menubar-height; 251 | background-color: white; 252 | border-bottom: 1px solid black; 253 | 254 | .menuitem { 255 | 256 | .menuitem-title { 257 | padding-left: 11px; 258 | padding-right: 11px; 259 | 260 | &.selected { 261 | color: white; 262 | background-color: black; 263 | } 264 | } 265 | 266 | .menufunctions { 267 | background-color: white; 268 | border: 1px solid black; 269 | 270 | .menufunction { 271 | padding-top: 6px; 272 | padding-bottom: 6px; 273 | padding-left: 11px; 274 | padding-right: 11px; 275 | width: 140px; 276 | 277 | &.hovered { 278 | background-color: black; 279 | color: white; 280 | } 281 | } 282 | } 283 | } 284 | } -------------------------------------------------------------------------------- /src/www/themes/Fluent.scss: -------------------------------------------------------------------------------- 1 | // MENUBAR HEIGHT 2 | $menubar-height: 36px; 3 | 4 | // COLORS 5 | $primary-color: #0078cf; 6 | $dgrey-color: #c2c2c2; 7 | $mgrey-color: #b5b5b6; 8 | $lgrey-color: #f2f2f2; 9 | $background-color: #f6f5f4; 10 | 11 | // GRADIENTS 12 | $button-gradient: linear-gradient($background-color, #e8e9ea); 13 | $disabled-gradient: linear-gradient($lgrey-color, $lgrey-color); 14 | 15 | // MARGINS 16 | $widget-margin: 6px; 17 | 18 | // PATH 19 | // Base 64 encoded string from : 20 | // 21 | $checkbox-path: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PScwIDAgMTAwIDEwMCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cG9seWdvbiBwb2ludHM9JzMwIDc1LCAxMDAgOCwgMTAwIDI0LCAzMCA4OSwgMCA2MCwgMCA0NCcgZmlsbD0nd2hpdGUnPjwvcG9seWdvbj48L3N2Zz4="); 22 | 23 | // Base 64 encoded string from : 24 | // 25 | $combo-path: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PScwIDAgMTAwIDEwMCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cG9seWdvbiBwb2ludHM9JzUwIDYwLCA4MCAzNSwgODAgNDUsIDUwIDcwLCAyMCA0NSwgMjAgMzUnIGZpbGw9J2JsYWNrJz48L3BvbHlnb24+PC9zdmc+"); 26 | 27 | 28 | #app { 29 | font-family: 'Verdana', sans-serif; 30 | font-size: 13px; 31 | background-color: $background-color; 32 | } 33 | 34 | .label { 35 | margin: $widget-margin; 36 | font-size: inherit; 37 | font-family: inherit; 38 | } 39 | 40 | .button { 41 | padding: 7px; 42 | margin: $widget-margin; 43 | border: 1px solid $mgrey-color; 44 | background: $mgrey-color; 45 | color: black; 46 | outline: 0; 47 | 48 | &.disabled { 49 | color: lighten(black, 50%); 50 | } 51 | 52 | img { 53 | height: 18px; 54 | width: 18px; 55 | 56 | + span { 57 | margin-left: 10px; 58 | } 59 | 60 | } 61 | } 62 | 63 | .progressbar { 64 | margin-top: 14px; 65 | margin-bottom: 14px; 66 | margin-left: $widget-margin; 67 | margin-right: $widget-margin; 68 | height: 4px; 69 | 70 | .background { 71 | background-color: $mgrey-color; 72 | border: 1px solid $mgrey-color; 73 | } 74 | 75 | .foreground { 76 | background-color: $primary-color; 77 | } 78 | } 79 | 80 | .textinput { 81 | margin: $widget-margin; 82 | 83 | input { 84 | border: 2px solid $mgrey-color; 85 | margin: 0; 86 | padding: 7px; 87 | font-size: inherit; 88 | font-family: inherit; 89 | outline: 0; 90 | 91 | &:focus { 92 | border-color: $primary-color; 93 | } 94 | } 95 | 96 | &.disabled { 97 | background-color: $mgrey-color; 98 | 99 | input { 100 | color: lighten(black, 20%); 101 | background-color: $mgrey-color; 102 | } 103 | } 104 | } 105 | 106 | .checkbox { 107 | margin: $widget-margin; 108 | 109 | label { 110 | margin-left: $widget-margin; 111 | } 112 | 113 | .checkbox-outer { 114 | height: 14px; 115 | width: 14px; 116 | background-color: white; 117 | border: 2px solid black; 118 | 119 | .checkbox-inner { 120 | width: 14px; 121 | height: 14px; 122 | background-image: $checkbox-path; 123 | background-size: 14px 14px; 124 | background-color: $primary-color; 125 | visibility: hidden; 126 | } 127 | } 128 | 129 | &.checked { 130 | .checkbox-outer { 131 | border-color: $primary-color; 132 | 133 | .checkbox-inner { 134 | visibility: visible; 135 | } 136 | } 137 | } 138 | 139 | &.disabled { 140 | .checkbox-outer { 141 | border-color: $mgrey-color; 142 | background-color: $mgrey-color; 143 | } 144 | 145 | label { 146 | color: lighten(black, 50%); 147 | } 148 | 149 | &.checked { 150 | .checkbox-outer { 151 | 152 | .checkbox-inner { 153 | background-color: transparent; 154 | visibility: visible; 155 | opacity: 0.5; 156 | } 157 | } 158 | } 159 | } 160 | } 161 | 162 | .radio { 163 | margin: $widget-margin; 164 | 165 | label { 166 | margin-left: $widget-margin; 167 | } 168 | 169 | .radio-outer { 170 | height: 14px; 171 | width: 14px; 172 | background-color: white; 173 | border: 2px solid black; 174 | border-radius: 50%; 175 | 176 | .radio-inner { 177 | height: 8px; 178 | width: 8px; 179 | border-radius: 50%; 180 | background-color: white; 181 | } 182 | } 183 | 184 | &.selected { 185 | .radio-outer { 186 | border-color: $primary-color; 187 | 188 | .radio-inner { 189 | background-color: black; 190 | } 191 | } 192 | } 193 | 194 | &.disabled { 195 | .radio-outer { 196 | border-color: $mgrey-color; 197 | } 198 | 199 | label { 200 | color: lighten(black, 50%); 201 | } 202 | 203 | &.selected { 204 | .radio-outer { 205 | 206 | .radio-inner { 207 | visibility: visible; 208 | opacity: 0.5; 209 | } 210 | } 211 | } 212 | } 213 | } 214 | 215 | .combo { 216 | margin: $widget-margin; 217 | 218 | .combo-button { 219 | padding: 7px; 220 | border: 2px solid $mgrey-color; 221 | color: black; 222 | outline: 0; 223 | background: white; 224 | 225 | .combo-icon { 226 | margin-left: 10px; 227 | height: 18px; 228 | width: 18px; 229 | background-image: $combo-path; 230 | background-size: 18px 18px; 231 | opacity: 0.8; 232 | } 233 | } 234 | 235 | .combo-choices { 236 | border: 1px solid $mgrey-color; 237 | border-top: none; 238 | 239 | .combo-choice { 240 | background-color: $lgrey-color; 241 | padding: 10px; 242 | 243 | &:hover { 244 | background-color: $mgrey-color; 245 | } 246 | } 247 | } 248 | 249 | &.disabled { 250 | 251 | .combo-button { 252 | color: lighten(black, 50%); 253 | background-color: $mgrey-color; 254 | 255 | .combo-icon { 256 | opacity: 0.25; 257 | } 258 | } 259 | } 260 | } 261 | 262 | .range { 263 | margin-left: $widget-margin; 264 | margin-right: $widget-margin; 265 | height: 32px; 266 | 267 | .inner-range { 268 | background-color: white; 269 | 270 | &::-webkit-slider-runnable-track { 271 | height: 4px; 272 | background-color: $mgrey-color; 273 | border: 1px solid $mgrey-color; 274 | } 275 | 276 | &::-webkit-slider-thumb { 277 | width:6px; 278 | height: 20px; 279 | background: $primary-color; 280 | border: 1px solid $primary-color; 281 | border-radius: 3px; 282 | margin-top: -9px; 283 | } 284 | 285 | &::-ms-track { 286 | margin-top: 10px; 287 | margin-bottom: 10px; 288 | height: 2px; 289 | background-color: $mgrey-color; 290 | border: 1px solid $mgrey-color; 291 | } 292 | 293 | &::-ms-thumb { 294 | width: 4px; 295 | height: 18px; 296 | background: $primary-color; 297 | border: 1px solid $primary-color; 298 | border-radius: 3px; 299 | margin-top: 0px; 300 | } 301 | } 302 | 303 | &.disabled { 304 | .inner-range { 305 | &::-webkit-slider-thumb { 306 | background: $mgrey-color; 307 | border-color: $mgrey-color; 308 | } 309 | 310 | &::-ms-thumb { 311 | background: $mgrey-color; 312 | border-color: $mgrey-color; 313 | } 314 | } 315 | } 316 | } 317 | 318 | .tabs { 319 | 320 | &.direction-vertical { 321 | 322 | .tab-titles { 323 | border: none; 324 | 325 | .tab-title { 326 | color: black; 327 | border: none; 328 | border-left: 3px solid $lgrey-color; 329 | 330 | &.selected { 331 | border: none; 332 | border-left: 3px solid $primary-color; 333 | } 334 | } 335 | } 336 | 337 | .tab { 338 | border: none; 339 | border-left: 1px solid $lgrey-color; 340 | } 341 | } 342 | 343 | .tab-titles { 344 | background-color: $lgrey-color; 345 | 346 | .tab-title { 347 | color: lighten(black, 50%); 348 | height: 40px; 349 | padding-left: 10px; 350 | padding-right: 10px; 351 | 352 | &.selected { 353 | color: black; 354 | border-bottom: 3px solid $primary-color; 355 | border-top: 3px solid $lgrey-color; 356 | } 357 | } 358 | } 359 | 360 | .tab { 361 | background-color: white; 362 | } 363 | } 364 | 365 | #app { 366 | .menubar ~ * { 367 | top: $menubar-height; 368 | } 369 | } 370 | 371 | .menubar { 372 | height: $menubar-height; 373 | background-color: white; 374 | 375 | .menuitem { 376 | 377 | .menuitem-title { 378 | box-sizing: border-box; 379 | padding-left: 10px; 380 | padding-right: 10px; 381 | 382 | &.selected { 383 | background-color: $mgrey-color; 384 | } 385 | } 386 | 387 | .menufunctions { 388 | background-color: $lgrey-color; 389 | border: 1px solid $mgrey-color; 390 | 391 | .menufunction { 392 | padding-top: 10px; 393 | padding-bottom: 10px; 394 | padding-left: 10px; 395 | padding-right: 10px; 396 | width: 140px; 397 | 398 | &:hover { 399 | background-color: $mgrey-color; 400 | } 401 | } 402 | } 403 | } 404 | } --------------------------------------------------------------------------------