From e95dbf7a6b2181139d1a68163a4fb9a50de00473 Mon Sep 17 00:00:00 2001 From: "Artem V. Ageev" Date: Fri, 19 Jul 2024 14:43:43 +0300 Subject: [PATCH] add ureg to dialect --- demos/Cargo.toml | 1 + demos/calculator/src/main.rs | 2 +- demos/calculator/src/model/mod.rs | 3 - demos/dialect/Cargo.toml | 13 ++ demos/dialect/README.md | 5 + demos/dialect/src/main.rs | 334 ++++++++++++++++++++++++++++++ demos/dialect/src/model/mod.rs | 77 +++++++ demos/resters/src/main.rs | 89 ++++---- demos/resters/src/model/mod.rs | 22 -- 9 files changed, 483 insertions(+), 63 deletions(-) create mode 100644 demos/dialect/Cargo.toml create mode 100644 demos/dialect/README.md create mode 100644 demos/dialect/src/main.rs create mode 100644 demos/dialect/src/model/mod.rs diff --git a/demos/Cargo.toml b/demos/Cargo.toml index ba6a694..cfb720a 100644 --- a/demos/Cargo.toml +++ b/demos/Cargo.toml @@ -6,6 +6,7 @@ members = [ "cairo_shadow_button", "calculator", "csv", + "dialect", "fltodo", "flightbooker", "resters", diff --git a/demos/calculator/src/main.rs b/demos/calculator/src/main.rs index 864e4c7..ddf4d58 100644 --- a/demos/calculator/src/main.rs +++ b/demos/calculator/src/main.rs @@ -143,7 +143,7 @@ impl Sandbox for Model { match message { Message::Quit => { let file = app::GlobalState::::get().with(move |file| file.clone()); - self.save(&file); + fs::write(file, rmp_serde::to_vec(&self).unwrap()).unwrap(); app::quit(); } Message::Theme => self.theme = !self.theme, diff --git a/demos/calculator/src/model/mod.rs b/demos/calculator/src/model/mod.rs index dbb725f..3507477 100644 --- a/demos/calculator/src/model/mod.rs +++ b/demos/calculator/src/model/mod.rs @@ -31,9 +31,6 @@ impl Model { default } } - pub fn save(&mut self, file: &str) { - fs::write(file, rmp_serde::to_vec(&self).unwrap()).unwrap(); - } pub fn click(&mut self, value: &str) { match value { "/" | "x" | "+" | "-" | "%" => { diff --git a/demos/dialect/Cargo.toml b/demos/dialect/Cargo.toml new file mode 100644 index 0000000..f080b67 --- /dev/null +++ b/demos/dialect/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dialect" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +flemish = { path = "../../" } +rmp-serde = { version="1.1" } +serde = { version="1.0", features = ["derive"] } +serde_json = "1" +ureq = { version = "2.9", features = ["json"] } diff --git a/demos/dialect/README.md b/demos/dialect/README.md new file mode 100644 index 0000000..81b63bd --- /dev/null +++ b/demos/dialect/README.md @@ -0,0 +1,5 @@ +# Dialect demo + +It's just dialect. + +![img](https://github.com/fltk-rs/demos/tree/master/fldialect/assets/fldialect.gif) diff --git a/demos/dialect/src/main.rs b/demos/dialect/src/main.rs new file mode 100644 index 0000000..594daa5 --- /dev/null +++ b/demos/dialect/src/main.rs @@ -0,0 +1,334 @@ +#![forbid(unsafe_code)] + +mod model; + +use { + flemish::{ + app, + button::Button, + color_themes, + dialog::{alert_default, FileChooser, FileChooserType, HelpDialog}, + enums::{Color, Event, Font, FrameType, Shortcut}, + frame::Frame, + group::Flex, + menu::{Choice, MenuButton, MenuFlag}, + prelude::*, + text::{TextBuffer, TextDisplay, TextEditor, WrapMode}, + valuator::{Counter, CounterType}, + OnEvent, OnMenuEvent, Sandbox, Settings, + }, + model::Model, + std::{env, fs, path::Path, process::Command, thread}, +}; + +fn main() { + if crate::once() { + app::GlobalState::::new(env::var("HOME").unwrap() + "/.config" + NAME); + Model::new().run(Settings { + ignore_esc_close: true, + resizable: false, + size: (360, 640), + color_map: Some(color_themes::DARK_THEME), + scheme: Some(app::Scheme::Base), + ..Default::default() + }); + } +} + +#[derive(Clone)] +pub enum Message { + Switch, + From(i32), + To(i32), + Source(String), + Size(i32), + Font(i32), + Translate, + Info, + Open, + Save, + Quit, +} + +impl Sandbox for Model { + type Message = Message; + + fn title(&self) -> String { + format!( + "Translate from {} to {} - {NAME}", + self.lang[self.from as usize]["name"], self.lang[self.to as usize]["name"] + ) + } + + fn new() -> Self { + let file = app::GlobalState::::get().with(move |file| file.clone()); + Model::default(&file) + } + + fn view(&mut self) { + let mut page = Flex::default_fill().column(); + { + let mut header = Flex::default(); + crate::menu(&mut header); + Frame::default(); + let lang = self + .lang + .iter() + .map(|x| x["name"].clone()) + .collect::>() + .join("|"); + crate::choice("From", &lang, self.from, &mut header) + .on_event(move |choice| Message::From(choice.value())); + crate::button("Switch", "@#refresh", &mut header).on_event(move |_| Message::Switch); + crate::choice("To", &lang, self.to, &mut header) + .on_event(move |choice| Message::To(choice.value())); + Frame::default(); + crate::button("Translate", "@#circle", &mut header) + .on_event(move |_| Message::Translate); + header.end(); + header.set_pad(0); + page.fixed(&header, HEIGHT); + } + { + let mut hero = Flex::default_fill().column(); + crate::texteditor("Source", &self.source, self.font, self.size) + .on_event(move |text| Message::Source(text.buffer().unwrap().text())); + hero.fixed(&Frame::default(), PAD); + crate::textdisplay("Target", &self.target, self.font, self.size); + hero.end(); + hero.set_pad(0); + } + { + let mut footer = Flex::default(); //FOOTER + crate::choice("Font", &app::fonts().join("|"), self.font, &mut footer) + .on_event(move |choice| Message::Font(choice.value())); + Frame::default(); + crate::counter("Size", self.size as f64, &mut footer) + .with_type(CounterType::Simple) + .on_event(move |counter| Message::Size(counter.value() as i32)); + footer.end(); + footer.set_pad(0); + page.fixed(&footer, HEIGHT); + } + page.end(); + page.set_margin(PAD); + page.set_pad(PAD); + page.set_frame(FrameType::FlatBox); + } + + fn update(&mut self, message: Message) { + match message { + Message::Quit => { + let file = app::GlobalState::::get().with(move |file| file.clone()); + self.save(&file); + app::quit(); + } + Message::From(value) => self.from = value, + Message::To(value) => self.to = value, + Message::Source(value) => self.source = value, + Message::Switch => std::mem::swap(&mut self.from, &mut self.to), + Message::Font(value) => self.font = value, + Message::Size(value) => self.size = value, + Message::Info => crate::info(), + Message::Open => { + let mut dialog = FileChooser::new( + env::var("HOME").unwrap(), + "*.{txt,md}", + FileChooserType::Single, + "Open ...", + ); + dialog.show(); + while dialog.shown() { + app::wait(); + } + if dialog.count() > 0 { + if let Some(file) = dialog.value(1) { + self.source = fs::read_to_string(Path::new(&file)).unwrap(); + }; + }; + } + Message::Save => { + if !self.target.is_empty() { + let mut dialog = FileChooser::new( + std::env::var("HOME").unwrap(), + "*.{txt,md}", + FileChooserType::Create, + "Save ...", + ); + dialog.show(); + while dialog.shown() { + app::wait(); + } + if dialog.count() > 0 { + if let Some(file) = dialog.value(1) { + fs::write(file, self.target.as_bytes()).unwrap(); + }; + }; + } else { + alert_default("Target is empty."); + }; + } + Message::Translate => { + let clone = self.clone(); + if clone.from != clone.to && !clone.source.is_empty() { + let handler = thread::spawn(move || -> String { clone.click() }); + while !handler.is_finished() { + app::wait(); + app::handle_main(SPINNER).unwrap(); + app::sleep(0.02); + } + if let Ok(text) = handler.join() { + self.target = text; + }; + }; + } + } + } +} + +fn button(tooltip: &str, label: &str, flex: &mut Flex) -> Button { + let mut element = Button::default().with_label(label); + element.set_tooltip(tooltip); + element.set_label_size(HEIGHT / 2); + flex.fixed(&element, HEIGHT); + element +} + +fn counter(tooltip: &str, value: f64, flex: &mut Flex) -> Counter { + let mut element = Counter::default(); + element.set_tooltip(tooltip); + element.set_range(14_f64, 22_f64); + element.set_precision(0); + element.set_value(value); + flex.fixed(&element, WIDTH - HEIGHT); + element +} + +fn info() { + const INFO: &str = r#"

+FlDialect + is similar to + Dialect + written using + FLTK-RS +

"#; + let mut dialog = HelpDialog::default(); + dialog.set_value(INFO); + dialog.set_text_size(16); + dialog.show(); + while dialog.shown() { + app::wait(); + } +} + +fn choice(tooltip: &str, choice: &str, value: i32, flex: &mut Flex) -> Choice { + let mut element = Choice::default(); + element.set_tooltip(tooltip); + element.add_choice(choice); + element.set_value(value); + flex.fixed(&element, WIDTH); + element +} + +fn texteditor(tooltip: &str, value: &str, font: i32, size: i32) -> TextEditor { + let mut element = TextEditor::default(); + element.set_tooltip(tooltip); + element.set_linenumber_width(HEIGHT); + element.set_buffer(TextBuffer::default()); + element.wrap_mode(WrapMode::AtBounds, 0); + element.buffer().unwrap().set_text(value); + element.set_color(Color::from_hex(0x002b36)); + element.set_text_color(Color::from_hex(0x93a1a1)); + element.set_text_font(Font::by_index(font as usize)); + element.set_text_size(size); + element.set_linenumber_size(size); + element +} + +fn textdisplay(tooltip: &str, value: &str, font: i32, size: i32) { + let mut element = TextDisplay::default(); + element.set_tooltip(tooltip); + element.set_linenumber_width(HEIGHT); + element.set_buffer(TextBuffer::default()); + element.wrap_mode(WrapMode::AtBounds, 0); + element.buffer().unwrap().set_text(value); + element.set_color(Color::from_hex(0x002b36)); + element.set_text_color(Color::from_hex(0x93a1a1)); + element.set_text_font(Font::by_index(font as usize)); + element.set_text_size(size); + element.set_linenumber_size(size); + element.handle(move |display, event| { + if event == crate::SPINNER { + display.insert("#"); + true + } else { + false + } + }); +} + +fn menu(flex: &mut Flex) { + let element = MenuButton::default(); + element + .clone() + .on_item_event( + "@#fileopen &Open...", + Shortcut::Ctrl | 'o', + MenuFlag::Normal, + move |_| Message::Open, + ) + .clone() + .on_item_event( + "@#filesaveas &Save as...", + Shortcut::Ctrl | 's', + MenuFlag::Normal, + move |_| Message::Save, + ) + .clone() + .on_item_event( + "@#circle T&ranslate", + Shortcut::Ctrl | 'r', + MenuFlag::Normal, + move |_| Message::Translate, + ) + .clone() + .on_item_event( + "@#search &Info", + Shortcut::Ctrl | 'i', + MenuFlag::Normal, + move |_| Message::Info, + ) + .on_item_event( + "@#1+ &Quit", + Shortcut::Ctrl | 'q', + MenuFlag::Normal, + move |_| Message::Quit, + ); + flex.fixed(&element, HEIGHT); +} + +pub fn once() -> bool { + if cfg!(target_os = "linux") { + let run = Command::new("lsof") + .args(["-t", env::current_exe().unwrap().to_str().unwrap()]) + .output() + .expect("failed to execute bash"); + match run.status.success() { + true => { + String::from_utf8_lossy(&run.stdout) + .split_whitespace() + .count() + == 1 + } + false => panic!("\x1b[31m{}\x1b[0m", String::from_utf8_lossy(&run.stderr)), + } + } else { + true + } +} + +const SPINNER: Event = Event::from_i32(405); +const NAME: &str = "FlDialect"; +const PAD: i32 = 10; +const HEIGHT: i32 = PAD * 3; +const WIDTH: i32 = 125; diff --git a/demos/dialect/src/model/mod.rs b/demos/dialect/src/model/mod.rs new file mode 100644 index 0000000..2f00299 --- /dev/null +++ b/demos/dialect/src/model/mod.rs @@ -0,0 +1,77 @@ +use { + serde::{Deserialize, Serialize}, + std::{collections::HashMap, fs}, +}; + +#[derive(Deserialize)] +struct Lang { + languages: Vec>, +} + +impl Lang { + const LINGVA: &'static str = r#"https://lingva.thedaviddelta.com/api/v1/"#; + fn init() -> Vec> { + if let Ok(response) = ureq::get(&format!("{}languages", Self::LINGVA)).call() { + response.into_json::().unwrap().languages + } else { + Vec::new() + } + } + fn tran(source: String, target: String, query: String) -> String { + if let Ok(response) = + ureq::get(&format!("{}/{}/{}/{}", Self::LINGVA, source, target, query)).call() + { + response + .into_json::() + .unwrap() + .as_object() + .unwrap()["translation"] + .to_string() + } else { + String::new() + } + } +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Model { + pub from: i32, + pub to: i32, + pub font: i32, + pub size: i32, + pub source: String, + pub target: String, + pub lang: Vec>, +} + +impl Model { + pub fn default(file: &str) -> Self { + let default = Self { + from: 0, + to: 0, + font: 1, + size: 14, + source: String::new(), + target: String::new(), + lang: Lang::init(), + }; + if let Ok(value) = fs::read(file) { + if let Ok(value) = rmp_serde::from_slice(&value) { + value + } else { + default + } + } else { + default + } + } + pub fn click(&self) -> String { + let from = self.lang[self.from as usize]["code"].clone(); + let to = self.lang[self.to as usize]["code"].clone(); + let source = self.source.clone(); + Lang::tran(from, to, source) + } + pub fn save(&mut self, file: &str) { + fs::write(file, rmp_serde::to_vec(&self).unwrap()).unwrap(); + } +} diff --git a/demos/resters/src/main.rs b/demos/resters/src/main.rs index 6024c0c..71d6573 100644 --- a/demos/resters/src/main.rs +++ b/demos/resters/src/main.rs @@ -7,22 +7,19 @@ use { app, button::Button, color_themes, - enums::{Align, Color, Event, Font, FrameType}, + enums::{Color, Event, Font, FrameType}, frame::Frame, - misc::InputChoice, group::Flex, menu::Choice, + misc::{InputChoice,Progress}, prelude::*, text::{StyleTableEntry, TextBuffer, TextDisplay, WrapMode}, OnEvent, Sandbox, Settings, - valuator::Dial, }, - std::{process::Command, thread}, + json_tools::{Buffer, BufferType, Lexer, Span, TokenType}, model::Model, }; -const SPINNER: Event = Event::from_i32(405); - fn main() { Model::new().run(Settings { size: (640, 360), @@ -49,14 +46,15 @@ impl Sandbox for Model { } fn title(&self) -> String { - String::from("flResters") + String::from("FlResters") } fn view(&mut self) { let mut page = Flex::default_fill().column(); let mut header = Flex::default(); header.fixed(&Frame::default(), WIDTH); - crate::choice(self.method as i32, &mut header).with_label("Method: ") + crate::choice(self.method as i32, &mut header) + .with_label("Method: ") .on_event(move |choice| Message::Method(choice.value() as u8)); header.fixed(&Frame::default(), WIDTH); crate::input(&self.url).on_event(move |input| Message::Url(input.value().unwrap())); @@ -66,13 +64,7 @@ impl Sandbox for Model { let mut footer = Flex::default(); footer.fixed(&Frame::default().with_label("Status: "), WIDTH); Frame::default(); - footer.fixed( - &Frame::default() - .with_align(Align::Left | Align::Inside) - .with_label(&self.status), - WIDTH, - ); - footer.fixed(&crate::dial(), HEIGHT); + footer.fixed(&crate::progress().with_label(&self.status), WIDTH); footer.end(); page.end(); { @@ -91,7 +83,7 @@ impl Sandbox for Model { Message::Url(value) => self.url = value, Message::Thread => { let clone = self.clone(); - let handler = thread::spawn(move || -> (bool, String) { crate::curl(clone) }); + let handler = std::thread::spawn(move || -> (bool, String) { crate::curl(clone) }); while !handler.is_finished() { app::wait(); app::handle_main(SPINNER).unwrap(); @@ -115,32 +107,30 @@ fn curl(model: Model) -> (bool, String) { true => model.url.clone(), false => String::from("https://") + &model.url, }; - let run = Command::new("curl") - .args(["-s", &url]) - .output() - .expect("failed to execute bash"); - ( - run.status.success(), - String::from_utf8_lossy(match run.status.success() { - true => &run.stdout, - false => &run.stderr, - }) - .to_string(), - ) + if let Ok(response) = match model.method { + 0 => ureq::get(&url), + 1 => ureq::post(&url), + _ => unreachable!(), + } + .call() + { + (true, response.into_string().unwrap()) + } else { + (false, String::from("Error")) + } } -fn dial() -> Dial { +fn progress() -> Progress { const MAX: u8 = 120; - let mut element = Dial::default(); - // element.deactivate(); + let mut element = Progress::default(); element.set_maximum((MAX / 4 * 3) as f64); element.set_value(element.minimum()); - element.handle(move |dial, event| { + element.handle(move |progress, event| { if event == crate::SPINNER { - dial.set_value(if dial.value() == (MAX - 1) as f64 { - dial.minimum() + progress.set_value(if progress.value() == (MAX - 1) as f64 { + progress.minimum() } else { - dial.value() + 1f64 + progress.value() + 1f64 }); true } else { @@ -160,7 +150,7 @@ fn choice(value: i32, flex: &mut Flex) -> Choice { fn text(value: &str) { let mut buffer = TextBuffer::default(); - buffer.set_text(&model::fill_style_buffer(value)); + buffer.set_text(&crate::fill_style_buffer(value)); let styles: Vec = [0xdc322f, 0x268bd2, 0x859900] .into_iter() .map(|color| StyleTableEntry { @@ -189,12 +179,37 @@ fn input(value: &str) -> InputChoice { for item in ["users", "posts", "albums", "todos", "comments", "posts"] { element.add(&(format!(r#"https:\/\/jsonplaceholder.typicode.com\/{item}"#))); } - element.add(r#"https:\/\/lingva.ml\/api\/v1\/languages"#); + element.add(r#"https:\/\/lingva.thedaviddelta.com\/api\/v1\/languages"#); element.add(r#"https:\/\/ipinfo.io\/json"#); element.set_value(value); element } +pub fn fill_style_buffer(s: &str) -> String { + let mut buffer = vec![b'A'; s.len()]; + for token in Lexer::new(s.bytes(), BufferType::Span) { + let c = match token.kind { + TokenType::CurlyOpen + | TokenType::CurlyClose + | TokenType::BracketOpen + | TokenType::BracketClose + | TokenType::Colon + | TokenType::Comma + | TokenType::Invalid => 'A', + TokenType::String => 'B', + TokenType::BooleanTrue | TokenType::BooleanFalse | TokenType::Null => 'C', + TokenType::Number => 'D', + }; + if let Buffer::Span(Span { first, end }) = token.buf { + let start = first as _; + let last = end as _; + buffer[start..last].copy_from_slice(c.to_string().repeat(last - start).as_bytes()); + } + } + String::from_utf8_lossy(&buffer).to_string() +} + +const SPINNER: Event = Event::from_i32(405); const PAD: i32 = 10; const HEIGHT: i32 = PAD * 3; const WIDTH: i32 = HEIGHT * 3; diff --git a/demos/resters/src/model/mod.rs b/demos/resters/src/model/mod.rs index 2c5a286..10c0bf0 100644 --- a/demos/resters/src/model/mod.rs +++ b/demos/resters/src/model/mod.rs @@ -1,5 +1,3 @@ -use json_tools::{TokenType,Span,BufferType,Buffer,Lexer}; - #[derive(Clone)] pub struct Model { pub method: u8, @@ -18,23 +16,3 @@ impl Model { } } } - -pub fn fill_style_buffer(s: &str) -> String { - let mut buffer = vec![b'A'; s.len()]; - for token in Lexer::new(s.bytes(), BufferType::Span) { - let c = match token.kind { - TokenType::CurlyOpen | TokenType::CurlyClose | TokenType::BracketOpen | TokenType::BracketClose | TokenType::Colon | TokenType::Comma | TokenType::Invalid => { - 'A' - } - TokenType::String => 'B', - TokenType::BooleanTrue | TokenType::BooleanFalse | TokenType::Null => 'C', - TokenType::Number => 'D', - }; - if let Buffer::Span(Span { first, end }) = token.buf { - let start = first as _; - let last = end as _; - buffer[start..last].copy_from_slice(c.to_string().repeat(last - start).as_bytes()); - } - } - String::from_utf8_lossy(&buffer).to_string() -}