diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a201d23..a83e1f0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -56,3 +56,59 @@ jobs: if [[ ${RUNNER_OS} == "Linux" ]]; then cargo build --quiet || cargo build --verbose fi + - name: Build base64 + working-directory: demos/base64 + shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi + - name: Build cairo_shadow_button + working-directory: demos/cairo_shadow_button + shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi + - name: Build calculator + working-directory: demos/calculator + shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi + - name: Build csv + working-directory: demos/csv + shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi + - name: Build dialect + working-directory: demos/dialect + shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi + - name: Build flightbooker + working-directory: demos/flightbooker + shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi + - name: Build resters + working-directory: demos/resters + shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi + - name: Build sudokusolver + working-directory: demos/sudokusolver + shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi diff --git a/Cargo.toml b/Cargo.toml index 7047b72..9abbed8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,5 @@ exclude = ["./examples"] [dependencies] fltk = { version = "1.4" } fltk-theme = "0.7" +fltk-table = "0.3" +fltk-grid = "0.4" diff --git a/README.md b/README.md index 9558916..5b30d50 100644 --- a/README.md +++ b/README.md @@ -77,65 +77,62 @@ To run the [examples:](/examples) cargo run --example counter cargo run --example temperature cargo run --example crud -cargo run --example flcalculator -cargo run --example fldialect cargo run --example flglyph cargo run --example flnetport cargo run --example flpicture -cargo run --example flresters ... ``` -### [FlCounter](/examples/counter.rs) +### [Counter](/examples/counter.rs) -![FlCalculator](/assets/counter.png) +![img](/assets/counter.png) -### [FlTemperature](/examples/temperature.rs) +### [Temperature](/examples/temperature.rs) -![FlTemperature](/assets/temperature.png) +![img](/assets/temperature.png) -### [FlCRUD](/examples/crud.rs) +### [CRUD](/examples/crud.rs) -![FlCRUD](/assets/crud.png) +![img](/assets/crud.png) -### [FlCalculator](/examples/flcalculator.rs) +### [Glyph](/examples/flglyph.rs) -![FlCalculator](/assets/flcalculator.gif) +![img](/assets/flglyph.png) -### [FlDialect](/examples/fldialect.rs) +### [NetPort](/examples/flnetport.rs) -![FlDialect](/assets/fldialect.gif) +![img](/assets/flnetport.png) -### [FlGlyph](/examples/flglyph.rs) +### [Picture](/examples/flpicture.rs) -![FlGlyph](/assets/flglyph.png) +![img](/assets/flpicture.gif) -### [FlNetPort](/examples/flnetport.rs) +## Demos -![FlNetPort](/assets/flnetport.png) +### [Cairo](/demos/cairo) -### [FlPicture](/examples/flpicture.rs) +![img](../demos/blob/master/cairo/assets/scrot.png) -![FlPicture](/assets/flpicture.gif) +### [Calculator](/demos/calculator) -### [FlResters](/examples/flresters.rs) +![img](../demos/blob/master/flcalculator/assets/flcalculator.gif) -![FlResters](/assets/flresters.png) +### [CSV](/demos/csv) -## Demos +![img](../demos/blob/master/csv/assets/csv.gif) -### [FlTodo](/demos/fltodo) +### [Dialect](/demos/dialect) -![FlTodo](/demos/fltodo/assets/fltodo.gif) +![img](../demos/blob/master/fldialect/assets/fldialect.gif) -### [FlCSV](/demos/csv) +### [Resters](/demos/resters) -![FlCSV](/demos/csv/assets/flcsv.png) +![img](../demos/blob/master/flresters/assets/flresters.gif) -### [FlCairo](/demos/cairo) +### [Todo](/demos/fltodo) -![FlCairo](/demos/cairo/assets/flcairo.png) +![img](/demos/fltodo/assets/fltodo.gif) ### [Flightbooker](/demos/flightbooker) -![Flightbooker](/demos/flightbooker/assets/flightbooker.png) +![img](/demos/flightbooker/assets/flightbooker.png) diff --git a/demos/Cargo.toml b/demos/Cargo.toml index ab0855b..4b92040 100644 --- a/demos/Cargo.toml +++ b/demos/Cargo.toml @@ -2,10 +2,17 @@ resolver = "2" members = [ + "base64", "cairo", + "cairo_shadow_button", + "calculator", "csv", - "fltodo", + "dialect", "flightbooker", + "resters", + "sudokusolver", + "text", + "todo", ] [profile.release] diff --git a/demos/csv/assets/historical_data/GME.csv b/demos/assets/historical_data/GME.csv similarity index 100% rename from demos/csv/assets/historical_data/GME.csv rename to demos/assets/historical_data/GME.csv diff --git a/demos/csv/assets/historical_data/dlpn.csv b/demos/assets/historical_data/dlpn.csv similarity index 100% rename from demos/csv/assets/historical_data/dlpn.csv rename to demos/assets/historical_data/dlpn.csv diff --git a/demos/csv/assets/historical_data/oil.csv b/demos/assets/historical_data/oil.csv similarity index 100% rename from demos/csv/assets/historical_data/oil.csv rename to demos/assets/historical_data/oil.csv diff --git a/demos/assets/logo.svg b/demos/assets/logo.svg new file mode 100644 index 0000000..7698044 --- /dev/null +++ b/demos/assets/logo.svg @@ -0,0 +1,24 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/demos/base64/Cargo.toml b/demos/base64/Cargo.toml new file mode 100644 index 0000000..f7ae268 --- /dev/null +++ b/demos/base64/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "b64converter" +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 = "../../" } +base64 = "0.22" diff --git a/demos/base64/README.md b/demos/base64/README.md new file mode 100644 index 0000000..604b0b6 --- /dev/null +++ b/demos/base64/README.md @@ -0,0 +1,5 @@ +# Base64 demo + +It's implementation that [program](https://github.com/andersjoern/b64converter/). + +![img](https://github.com/andersjoern/b64converter/blob/master/screenshots/encode_decode.png) diff --git a/demos/base64/src/main.rs b/demos/base64/src/main.rs new file mode 100644 index 0000000..689e9ad --- /dev/null +++ b/demos/base64/src/main.rs @@ -0,0 +1,128 @@ +mod model; + +use { + flemish::{ + button::Button, + color_themes, + enums::{Color, Event, Font}, + frame::Frame, + image::SvgImage, + group::{Flex, FlexType}, + prelude::*, + text::{TextBuffer, TextEditor, WrapMode}, + OnEvent, Sandbox, Settings, + }, + model::Model, +}; + +const PAD: i32 = 10; +const HEIGHT: i32 = PAD * 3; +const NAME: &str = "FlBase64"; + +#[derive(Clone)] +pub enum Message { + Encode, + Decode, + Source(String), + Target(String), +} + +fn main() { + Model::new().run(Settings { + resizable: true, + size: (360, 640), + xclass: Some(String::from(NAME)), + icon: Some(SvgImage::from_data(include_str!("../../assets/logo.svg")).unwrap()), + color_map: Some(color_themes::DARK_THEME), + ..Default::default() + }) +} + +impl Sandbox for Model { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from(NAME) + } + + fn view(&mut self) { + let mut page = Flex::default_fill().column(); + { + let mut hero = Flex::default_fill(); + { + crate::texteditor("Normal text", &self.decode, self.font, self.size) + .on_event(move |text| Message::Source(text.buffer().unwrap().text())); + Frame::default(); + crate::texteditor("Base64 text", &self.encode, self.font, self.size) + .on_event(move |text| Message::Target(text.buffer().unwrap().text())); + } + hero.end(); + hero.set_pad(0); + crate::orientation(&mut hero); + hero.handle(crate::resize); + let mut footer = Flex::default(); + { + crate::button("Decode", "@<-", &mut footer).on_event(move |_| Message::Decode); + Frame::default(); + crate::button("Encode", "@->", &mut footer).on_event(move |_| Message::Encode); + } + footer.end(); + page.fixed(&footer, HEIGHT); + } + page.end(); + page.set_pad(PAD); + page.set_margin(PAD); + } + + fn update(&mut self, message: Message) { + match message { + Message::Source(value) => self.decode = value, + Message::Target(value) => self.encode = value, + Message::Encode => self.encode(), + Message::Decode => self.decode(), + } + } +} + +fn texteditor(tooltip: &str, value: &str, font: i32, size: i32) -> TextEditor { + let mut element = TextEditor::default(); + element.set_tooltip(tooltip); + element.set_linenumber_width(0); + 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 +} + +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 resize(flex: &mut Flex, event: Event) -> bool { + if event == Event::Resize { + crate::orientation(flex); + flex.fixed(&flex.child(1).unwrap(), PAD); + true + } else { + false + } +} + +fn orientation(flex: &mut Flex) { + flex.set_type(match flex.width() < flex.height() { + true => FlexType::Column, + false => FlexType::Row, + }); +} diff --git a/demos/base64/src/model/mod.rs b/demos/base64/src/model/mod.rs new file mode 100644 index 0000000..cfa1adc --- /dev/null +++ b/demos/base64/src/model/mod.rs @@ -0,0 +1,29 @@ +use base64::{engine::general_purpose, Engine}; + +#[derive(Debug)] +pub struct Model { + pub decode: String, + pub encode: String, + pub font: i32, + pub size: i32, +} + +impl Model { + pub fn default() -> Self { + Self { + decode: String::from("Normal text"), + encode: String::from("Base64 text"), + font: 0, + size: 14, + } + } + pub fn encode(&mut self) { + self.encode = general_purpose::STANDARD.encode(&self.decode); + } + pub fn decode(&mut self) { + self.decode = match general_purpose::STANDARD.decode(&self.encode) { + Ok(decode) => String::from_utf8(decode).unwrap(), + Err(error) => format!("{}", error), + } + } +} diff --git a/demos/cairo/Cargo.toml b/demos/cairo/Cargo.toml index 92fb339..1c3ddf2 100644 --- a/demos/cairo/Cargo.toml +++ b/demos/cairo/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cairo_button" +name = "cairo_frame" version = "0.1.0" edition = "2021" @@ -7,5 +7,5 @@ edition = "2021" [dependencies] flemish = { path = "../../" } +fltk = { version = "^1.4", features = ["use-ninja", "cairoext"] } cairo-rs = "0.18" -cairo-blur = "^0.1" diff --git a/demos/cairo/README.md b/demos/cairo/README.md new file mode 100644 index 0000000..0009e0e --- /dev/null +++ b/demos/cairo/README.md @@ -0,0 +1,5 @@ +# Cairo demo + +Use Cairo for custom drawing. + +![img](https://github.com/fltk-rs/demos/blob/master/cairo/assets/scrot.png) diff --git a/demos/cairo/assets/flcairo.png b/demos/cairo/assets/flcairo.png deleted file mode 100644 index 9c24571..0000000 Binary files a/demos/cairo/assets/flcairo.png and /dev/null differ diff --git a/demos/cairo/src/main.rs b/demos/cairo/src/main.rs index 84a73ce..fc893d5 100644 --- a/demos/cairo/src/main.rs +++ b/demos/cairo/src/main.rs @@ -1,40 +1,22 @@ -#![forbid(unsafe_code)] - mod model; use { - cairo::{Context, Format, ImageSurface}, + cairo::Context, flemish::{ - app, - button::Button, - color_themes, draw, - enums::Event, - enums::{Align, Color, ColorDepth, Font, Shortcut}, - frame::Frame, - group::Flex, - image::RgbImage, - menu::{MenuButton, MenuButtonType, MenuFlag}, - prelude::*, - OnEvent, OnMenuEvent, Sandbox, Settings, + enums::{Color, Event}, frame::Frame, prelude::*, OnEvent, Sandbox, Settings, }, model::Model, }; #[derive(Clone, Copy)] pub enum Message { - Inc, - Dec, - Quit, + Change(usize), } fn main() { Model::new().run(Settings { - size: (640, 360), - ignore_esc_close: true, - resizable: false, - background: Some(Color::from_u32(0xfdf6e3)), - color_map: Some(color_themes::TAN_THEME), - scheme: Some(app::Scheme::Base), + size: (260, 260), + background: Some(Color::White), ..Default::default() }) } @@ -47,177 +29,72 @@ impl Sandbox for Model { } fn title(&self) -> String { - format!("{} - FlCairo", self.value()) + format!("{} - FlCairo", self.state[0]) } fn view(&mut self) { - let mut page = Flex::default() - .with_size(600, 200) - .center_of_parent() - .column(); - - let hero = Flex::default(); //HERO - crate::cairobutton() - .with_label("@#<") - .on_event(move |_| Message::Dec); - crate::frame(&self.value()).handle(crate::popup); - crate::cairobutton() - .with_label("@#>") - .on_event(move |_| Message::Inc); - hero.end(); - - page.end(); - page.set_pad(0); - page.set_margin(0); + fltk::app::cairo::set_autolink_context(true); + let mut frame = cairowidget(5, 5, 100, 100, "Box1"); + frame.set_color(match self.state[0] { + true => Color::Red, + false => Color::DarkRed, + }); + frame.handle(crate::proxy); + frame.on_event(move |_| Message::Change(0)); + let mut frame = cairowidget(80, 80, 100, 100, "Box2"); + frame.set_color(match self.state[1] { + true => Color::Yellow, + false => Color::DarkYellow, + }); + frame.handle(crate::proxy); + frame.on_event(move |_| Message::Change(1)); + let mut frame = cairowidget(155, 155, 100, 100, "Box3"); + frame.set_color(match self.state[2] { + true => Color::Green, + false => Color::DarkGreen, + }); + frame.handle(crate::proxy); + frame.on_event(move |_| Message::Change(2)); } fn update(&mut self, message: Message) { match message { - Message::Inc => self.inc(), - Message::Dec => self.dec(), - Message::Quit => app::quit(), + Message::Change(idx) => self.change(idx), } } } -fn menu() -> MenuButton { - MenuButton::default() - .with_type(MenuButtonType::Popup3) - .with_label("@#menu") - .on_item_event( - "@#+ &Increment", - Shortcut::Ctrl | 'i', - MenuFlag::Normal, - move |_| Message::Inc, - ) - .on_item_event( - "@#- &Decrement", - Shortcut::Ctrl | 'd', - MenuFlag::Normal, - move |_| Message::Dec, - ) - .on_item_event( - "@#1+ Quit", - Shortcut::Ctrl | 'q', - MenuFlag::Normal, - move |_| Message::Quit, - ) -} - -fn popup(_: &mut Frame, event: Event) -> bool { - match event { - Event::Push => match app::event_mouse_button() { - app::MouseButton::Right => { - crate::menu().popup(); - true - } - _ => false, - }, - _ => false, +fn proxy(frame: &mut Frame, event: Event) -> bool { + if event == Event::Released { + frame.do_callback(); + true + } else { + false } } -fn frame(value: &str) -> Frame { - let mut element = Frame::default().with_label(value); - element.set_label_size(60); - element -} - -fn cairobutton() -> Button { - let mut element = Button::default(); - element.super_draw(false); - element.draw(|button| { - draw::draw_rect_fill( - button.x(), - button.y(), - button.w(), - button.h(), - Color::from_u32(0xfdf6e3), - ); - let mut surface = ImageSurface::create(Format::ARgb32, button.w(), button.h()) - .expect("Couldn’t create surface"); - crate::draw_surface(&mut surface, button.w(), button.h()); - if !button.value() { - cairo_blur::blur_image_surface(&mut surface, 20); - } - surface - .with_data(|surface| { - RgbImage::new(surface, button.w(), button.h(), ColorDepth::Rgba8) - .unwrap() - .draw(button.x(), button.y(), button.w(), button.h()); - }) - .unwrap(); - draw::set_draw_color(Color::Black); - draw::set_font(Font::Helvetica, app::font_size()); - if !button.value() { - draw::draw_rbox( - button.x() + 1, - button.y() + 1, - button.w() - 6, - button.h() - 6, - 15, - true, - Color::White, - ); - draw::draw_text2( - &button.label(), - button.x() + 1, - button.y() + 1, - button.w() - 6, - button.h() - 6, - Align::Center, - ); - } else { - draw::draw_rbox( - button.x() + 1, - button.y() + 1, - button.w() - 4, - button.h() - 4, - 15, - true, - Color::White, - ); - draw::draw_text2( - &button.label(), - button.x() + 1, - button.y() + 1, - button.w() - 4, - button.h() - 4, - Align::Center, - ); - } - }); - element -} - -fn draw_surface(surface: &mut ImageSurface, w: i32, h: i32) { - let ctx = Context::new(surface).unwrap(); +fn draw_box_with_alpha(rect: &mut Frame) { + let ctx = unsafe { Context::from_raw_none(fltk::app::cairo::cc() as _) }; + let (r, g, b) = rect.color().to_rgb(); ctx.save().unwrap(); - let corner_radius = h as f64 / 10.0; - let radius = corner_radius / 1.0; - let degrees = std::f64::consts::PI / 180.0; - - ctx.new_sub_path(); - ctx.arc(w as f64 - radius, radius, radius, -90. * degrees, 0.0); - ctx.arc( - w as f64 - radius, - h as f64 - radius, - radius, - 0.0, - 90. * degrees, - ); - ctx.arc( - radius, - h as f64 - radius, - radius, - 90. * degrees, - 180. * degrees, - ); - ctx.arc(radius, radius, radius, 180. * degrees, 270. * degrees); + ctx.move_to(rect.x() as f64, rect.y() as f64); + ctx.line_to((rect.x() + rect.w()) as f64, rect.y() as f64); + ctx.line_to((rect.x() + rect.w()) as f64, (rect.y() + rect.h()) as f64); + ctx.line_to(rect.x() as f64, (rect.y() + rect.h()) as f64); ctx.close_path(); - - ctx.set_source_rgba(150.0 / 255.0, 150.0 / 255.0, 150.0 / 255.0, 40.0 / 255.0); - ctx.set_line_width(4.); + ctx.set_source_rgba( + r as f64 / 255.0, + g as f64 / 255.0, + b as f64 / 255.0, + 100.0 / 255.0, + ); ctx.fill().unwrap(); ctx.restore().unwrap(); } + +pub fn cairowidget(x: i32, y: i32, w: i32, h: i32, label: &str) -> Frame { + let mut element = Frame::new(x, y, w, h, None).with_label(label); + element.super_draw_first(false); // required for windows + element.draw(draw_box_with_alpha); + element +} diff --git a/demos/cairo/src/model/mod.rs b/demos/cairo/src/model/mod.rs index 0f65d50..e3716d1 100644 --- a/demos/cairo/src/model/mod.rs +++ b/demos/cairo/src/model/mod.rs @@ -1,18 +1,12 @@ pub struct Model { - value: u8, + pub state: [bool; 3], } impl Model { pub fn default() -> Self { - Self { value: 0u8 } + Self { state: [true; 3] } } - pub fn inc(&mut self) { - self.value = self.value.saturating_add(1); - } - pub fn dec(&mut self) { - self.value = self.value.saturating_sub(1); - } - pub fn value(&self) -> String { - self.value.to_string() + pub fn change(&mut self, idx: usize) { + self.state[idx] = !self.state[idx]; } } diff --git a/demos/cairo_shadow_button/Cargo.toml b/demos/cairo_shadow_button/Cargo.toml new file mode 100644 index 0000000..adc0aa9 --- /dev/null +++ b/demos/cairo_shadow_button/Cargo.toml @@ -0,0 +1,10 @@ +[package] +authors = ["Mohammed Alyousef "] +name = "cairo_button" +version = "0.1.0" +edition = "2021" + +[dependencies] +flemish = { path = "../../" } +cairo-rs = "0.18" +cairo-blur = "^0.1" diff --git a/demos/cairo_shadow_button/README.md b/demos/cairo_shadow_button/README.md new file mode 100644 index 0000000..0754df8 --- /dev/null +++ b/demos/cairo_shadow_button/README.md @@ -0,0 +1,5 @@ +# cairo shadow button + +This shows how to create a rounded button with a blur effect using cairo. + +![img](https://github.com/fltk-rs/demos/blob/master/cairo_shadow_button/assets/scrot.png) diff --git a/demos/cairo_shadow_button/src/main.rs b/demos/cairo_shadow_button/src/main.rs new file mode 100644 index 0000000..050d56d --- /dev/null +++ b/demos/cairo_shadow_button/src/main.rs @@ -0,0 +1,233 @@ +#![forbid(unsafe_code)] + +mod model; + +use { + cairo::{Context, Format, ImageSurface}, + flemish::{ + app, + button::Button, + draw, + enums::{Align, Color, ColorDepth, Event, Font, Shortcut}, + frame::Frame, + group::Flex, + image::{RgbImage,SvgImage}, + menu::{MenuButton, MenuButtonType, MenuFlag}, + prelude::*, + OnEvent, OnMenuEvent, Sandbox, Settings, + }, + model::Model, +}; + +#[derive(Clone, Copy)] +pub enum Message { + Inc, + Dec, + Quit, +} + +const NAME: &str = "FlCairoButton"; + +fn main() { + Model::new().run(Settings { + size: (640, 360), + resizable: false, + xclass: Some(String::from(NAME)), + icon: Some(SvgImage::from_data(include_str!("../../assets/logo.svg")).unwrap()), + background: Some(Color::from_u32(0xfdf6e3)), + on_close_fn: Some(Box::new(move |_| { + if app::event() == Event::Close { + let (s, _) = app::channel::(); + s.send(Message::Quit); + } + })), + ..Default::default() + }) +} + +impl Sandbox for Model { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + format!("{} - {NAME}", self.value()) + } + + fn view(&mut self) { + let mut page = Flex::default() + .with_size(600, 200) + .center_of_parent() + .column(); + + let hero = Flex::default(); //HERO + crate::cairobutton() + .with_label("@#<") + .on_event(move |_| Message::Dec); + crate::frame() + .with_label(&self.value()) + .handle(crate::popup); + crate::cairobutton() + .with_label("@#>") + .on_event(move |_| Message::Inc); + hero.end(); + + page.end(); + page.set_pad(0); + page.set_margin(0); + } + + fn update(&mut self, message: Message) { + match message { + Message::Inc => self.inc(), + Message::Dec => self.dec(), + Message::Quit => { + self.save(); + app::quit(); + } + } + } +} + +fn menu() -> MenuButton { + MenuButton::default() + .with_type(MenuButtonType::Popup3) + .on_item_event( + "@#+ &Increment", + Shortcut::Ctrl | 'i', + MenuFlag::Normal, + move |_| Message::Inc, + ) + .on_item_event( + "@#- &Decrement", + Shortcut::Ctrl | 'd', + MenuFlag::Normal, + move |_| Message::Dec, + ) + .on_item_event( + "@#1+ Quit", + Shortcut::Ctrl | 'q', + MenuFlag::Normal, + move |_| Message::Quit, + ) +} + +fn popup(_: &mut Frame, event: Event) -> bool { + match event { + Event::Push => match app::event_mouse_button() { + app::MouseButton::Right => { + crate::menu().popup(); + true + } + _ => false, + }, + _ => false, + } +} + +fn frame() -> Frame { + let mut element = Frame::default(); + element.set_label_size(60); + element +} + +fn cairobutton() -> Button { + let mut element = Button::default(); + element.super_draw(false); + element.draw(|button| { + draw::draw_rect_fill( + button.x(), + button.y(), + button.w(), + button.h(), + Color::from_u32(0xfdf6e3), + ); + let mut surface = ImageSurface::create(Format::ARgb32, button.w(), button.h()) + .expect("Couldn’t create surface"); + crate::draw_surface(&mut surface, button.w(), button.h()); + if !button.value() { + cairo_blur::blur_image_surface(&mut surface, 20); + } + surface + .with_data(|surface| { + RgbImage::new(surface, button.w(), button.h(), ColorDepth::Rgba8) + .unwrap() + .draw(button.x(), button.y(), button.w(), button.h()); + }) + .unwrap(); + draw::set_draw_color(Color::Black); + draw::set_font(Font::Helvetica, app::font_size()); + if !button.value() { + draw::draw_rbox( + button.x() + 1, + button.y() + 1, + button.w() - 6, + button.h() - 6, + 15, + true, + Color::White, + ); + draw::draw_text2( + &button.label(), + button.x() + 1, + button.y() + 1, + button.w() - 6, + button.h() - 6, + Align::Center, + ); + } else { + draw::draw_rbox( + button.x() + 1, + button.y() + 1, + button.w() - 4, + button.h() - 4, + 15, + true, + Color::White, + ); + draw::draw_text2( + &button.label(), + button.x() + 1, + button.y() + 1, + button.w() - 4, + button.h() - 4, + Align::Center, + ); + } + }); + element +} + +fn draw_surface(surface: &mut ImageSurface, w: i32, h: i32) { + let ctx = Context::new(surface).unwrap(); + ctx.save().unwrap(); + let corner_radius = h as f64 / 10.0; + let radius = corner_radius / 1.0; + let degrees = std::f64::consts::PI / 180.0; + + ctx.new_sub_path(); + ctx.arc(w as f64 - radius, radius, radius, -90. * degrees, 0.0); + ctx.arc( + w as f64 - radius, + h as f64 - radius, + radius, + 0.0, + 90. * degrees, + ); + ctx.arc( + radius, + h as f64 - radius, + radius, + 90. * degrees, + 180. * degrees, + ); + ctx.arc(radius, radius, radius, 180. * degrees, 270. * degrees); + ctx.close_path(); + + ctx.set_source_rgba(150.0 / 255.0, 150.0 / 255.0, 150.0 / 255.0, 40.0 / 255.0); + ctx.set_line_width(4.); + ctx.fill().unwrap(); + ctx.restore().unwrap(); +} diff --git a/demos/cairo_shadow_button/src/model/mod.rs b/demos/cairo_shadow_button/src/model/mod.rs new file mode 100644 index 0000000..f0ce606 --- /dev/null +++ b/demos/cairo_shadow_button/src/model/mod.rs @@ -0,0 +1,31 @@ +use std::{env, fs}; + +#[derive(Debug, Clone)] +pub struct Model { + value: u8, +} + +impl Model { + pub fn default() -> Self { + if let Ok(value) = fs::read(file()) { + return Self { value: value[0] }; + }; + Self { value: 0u8 } + } + pub fn inc(&mut self) { + self.value = self.value.saturating_add(1); + } + pub fn dec(&mut self) { + self.value = self.value.saturating_sub(1); + } + pub fn value(&self) -> String { + self.value.to_string() + } + pub fn save(&mut self) { + fs::write(file(), [self.value]).unwrap(); + } +} + +fn file() -> String { + env::var("HOME").unwrap() + "/.config/" + crate::NAME +} diff --git a/demos/calculator/Cargo.toml b/demos/calculator/Cargo.toml new file mode 100644 index 0000000..812d753 --- /dev/null +++ b/demos/calculator/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "calculate" +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 = "../../" } +serde = { version="1.0", features = ["derive"] } +rmp-serde = { version="1.1" } diff --git a/demos/calculator/README.md b/demos/calculator/README.md new file mode 100644 index 0000000..d10e225 --- /dev/null +++ b/demos/calculator/README.md @@ -0,0 +1,5 @@ +# Calculator demo + +It's just calculator. + +![img](https://github.com/fltk-rs/demos/tree/master/flcalculator/assets/flcalculator.gif) diff --git a/examples/flcalculator.rs b/demos/calculator/src/main.rs similarity index 54% rename from examples/flcalculator.rs rename to demos/calculator/src/main.rs index 43eaf2c..a1601d1 100644 --- a/examples/flcalculator.rs +++ b/demos/calculator/src/main.rs @@ -1,98 +1,69 @@ #![forbid(unsafe_code)] +mod model; use { flemish::{ app, button::Button, color_themes, - enums::{Align, Color, Event, Font, FrameType, Key, Shortcut}, + enums::{Align, Color, Cursor, Event, Font, FrameType, Key, Shortcut}, frame::Frame, group::Flex, + image::SvgImage, menu::{MenuButton, MenuButtonType, MenuFlag}, prelude::*, text::{TextBuffer, TextDisplay, WrapMode}, OnEvent, OnMenuEvent, Sandbox, Settings, }, - std::{env, fs, path::Path}, + model::Model, }; -pub fn main() { - app::GlobalState::::new(env::var("HOME").unwrap() + PATH + NAME); +const PAD: i32 = 10; +const HEIGHT: i32 = PAD * 3; +const EQUAL: &str = "="; +const COLORS: [[Color; 6]; 2] = [ + [ + Color::from_hex(0xfdf6e3), + Color::from_hex(0x586e75), + Color::from_hex(0xb58900), + Color::from_hex(0xeee8d5), + Color::from_hex(0xcb4b16), + Color::from_hex(0xdc322f), + ], + [ + Color::from_hex(0x002b36), + Color::from_hex(0x93a1a1), + Color::from_hex(0x268bd2), + Color::from_hex(0x073642), + Color::from_hex(0x6c71c4), + Color::from_hex(0xd33682), + ], +]; +const NAME: &str = "FlCalculator"; + +fn main() { Model::new().run(Settings { size: (360, 640), - resizable: false, - ignore_esc_close: true, + xclass: Some(String::from(NAME)), + icon: Some(SvgImage::from_data(include_str!("../../assets/logo.svg")).unwrap()), color_map: Some(color_themes::TAN_THEME), - scheme: Some(app::Scheme::Base), + on_close_fn: Some(Box::new(move |_| { + if app::event() == Event::Close { + let (s, _) = app::channel::(); + s.send(Message::Quit); + } + })), ..Default::default() }) } #[derive(PartialEq, Clone)] -enum Message { +pub enum Message { Click(String), Theme, Quit, } -#[derive(Clone)] -struct Model { - prev: String, - operation: String, - current: String, - output: String, - theme: bool, -} - -impl Model { - fn theme(&mut self) { - self.theme = !self.theme; - } - fn click(&mut self, value: String) { - match value.as_str() { - "/" | "x" | "+" | "-" | "%" => { - if self.operation.is_empty() { - self.operation.push_str(&value); - self.prev = self.current.clone(); - } else { - self.equil(); - self.operation = String::from("="); - } - self.output - .push_str(&format!("{} {}", self.prev, self.operation)); - self.current = String::from("0"); - } - "=" => self.equil(), - "CE" => { - self.output.clear(); - self.operation.clear(); - self.current = String::from("0"); - self.prev = String::from("0"); - } - "@<-" => { - let label = self.current.clone(); - self.current = if label.len() > 1 { - String::from(&label[..label.len() - 1]) - } else { - String::from("0") - }; - } - "C" => self.current = String::from("0"), - "." => { - if !self.current.contains('.') { - self.current.push('.'); - } - } - _ => { - if self.current == "0" { - self.current.clear(); - } - self.current = self.current.clone() + &value; - } - }; - } -} - impl Sandbox for Model { type Message = Message; @@ -101,18 +72,7 @@ impl Sandbox for Model { } fn new() -> Self { - let file = app::GlobalState::::get().with(move |model| model.clone()); - let theme: bool = match Path::new(&file).exists() { - true => fs::read(&file).unwrap()[0], - false => 0, - } != 0; - Self { - prev: String::from("0"), - operation: String::new(), - current: String::from("0"), - output: String::new(), - theme, - } + Model::default() } fn view(&mut self) { @@ -121,16 +81,22 @@ impl Sandbox for Model { crate::display("Output", &self.output, self.theme as usize); let mut row = Flex::default(); row.fixed( - &crate::output("Operation", &self.operation, self.theme as usize), + &crate::output("Operation", self.theme as usize).with_label(&self.operation), 30, ); let mut col = Flex::default().column(); - crate::output("Previous", &self.prev, self.theme as usize); - crate::output("Current", &self.current, self.theme as usize); + crate::output("Previous", self.theme as usize).with_label(&self.prev.to_string()); + crate::output("Current", self.theme as usize).with_label(&self.current); col.end(); row.end(); let mut buttons = Flex::default_fill().column(); - for line in BUTTONS { + for line in [ + ["CE", "C", "%", "/"], + ["7", "8", "9", "x"], + ["4", "5", "6", "-"], + ["1", "2", "3", "+"], + ["0", ".", "@<-", crate::EQUAL], + ] { let mut row = Flex::default(); for label in line { crate::button(label, self.theme as usize) @@ -148,7 +114,7 @@ impl Sandbox for Model { row.set_margin(0); buttons.set_pad(PAD); buttons.set_margin(0); - buttons.handle(move |_, event| match event { + buttons.handle(move |flex, event| match event { Event::Push => match app::event_mouse_button() { app::MouseButton::Right => { menu.popup(); @@ -156,6 +122,14 @@ impl Sandbox for Model { } _ => false, }, + Event::Enter => { + flex.window().unwrap().set_cursor(Cursor::Hand); + true + } + Event::Leave => { + flex.window().unwrap().set_cursor(Cursor::Arrow); + true + } _ => false, }); page.set_margin(PAD); @@ -171,45 +145,16 @@ impl Sandbox for Model { fn update(&mut self, message: Message) { match message { - Message::Quit => self.quit(), - Message::Theme => self.theme(), - Message::Click(value) => self.click(value), + Message::Quit => { + self.save(); + app::quit(); + } + Message::Theme => self.theme = !self.theme, + Message::Click(value) => self.click(&value), }; } } -impl Model { - fn quit(&self) { - let file = app::GlobalState::::get().with(move |model| model.clone()); - fs::write(file, [self.theme as u8]).unwrap(); - app::quit(); - } - fn equil(&mut self) { - if !self.operation.is_empty() { - let left: f64 = self.prev.parse().unwrap(); - let right: f64 = self.current.parse().unwrap(); - let temp = match self.operation.as_str() { - "/" => left / right, - "x" => left * right, - "+" => left + right, - "-" => left - right, - _ => left / 100.0 * right, - }; - self.output.push_str(&format!( - " {right}\n{} = {temp}\n", - (0..=left.to_string().len()) - .map(|_| ' ') - .collect::(), - )); - self.prev = temp.to_string(); - } else { - self.prev = self.current.clone(); - } - self.operation.clear(); - self.current = String::from("0"); - } -} - fn display(tooltip: &str, value: &str, theme: usize) { let mut element = TextDisplay::default(); element.set_tooltip(tooltip); @@ -219,22 +164,21 @@ fn display(tooltip: &str, value: &str, theme: usize) { element.set_scrollbar_size(3); element.set_frame(FrameType::FlatBox); element.wrap_mode(WrapMode::AtBounds, 0); - element.set_color(COLORS[theme as usize][0]); - element.set_text_color(COLORS[theme as usize][1]); + element.set_color(COLORS[theme][0]); + element.set_text_color(COLORS[theme][1]); element.scroll( element.buffer().unwrap().text().split_whitespace().count() as i32, 0, ); } -fn output(tooltip: &str, value: &str, theme: usize) -> Frame { +fn output(tooltip: &str, theme: usize) -> Frame { let mut element = Frame::default().with_align(Align::Right | Align::Inside); element.set_tooltip(tooltip); element.set_label_size(HEIGHT); - element.set_label(value); element.set_frame(FrameType::FlatBox); - element.set_color(COLORS[theme as usize][0]); - element.set_label_color(COLORS[theme as usize][1]); + element.set_color(COLORS[theme][0]); + element.set_label_color(COLORS[theme][1]); element } @@ -245,7 +189,7 @@ fn button(label: &'static str, theme: usize) -> Button { match label { "@<-" => element.set_shortcut(Shortcut::None | Key::BackSpace), "CE" => element.set_shortcut(Shortcut::None | Key::Delete), - "=" => element.set_shortcut(Shortcut::None | Key::Enter), + crate::EQUAL => element.set_shortcut(Shortcut::None | Key::Enter), "x" => element.set_shortcut(Shortcut::None | '*'), _ => element.set_shortcut(Shortcut::None | label.chars().next().unwrap()), } @@ -258,7 +202,7 @@ fn button(label: &'static str, theme: usize) -> Button { element.set_color(COLORS[theme][4]); element.set_label_color(COLORS[theme][0]); } - "=" => { + crate::EQUAL => { element.set_color(COLORS[theme][5]); element.set_label_color(COLORS[theme][0]); } @@ -293,37 +237,7 @@ pub fn menu(theme: usize) -> MenuButton { move |_| Message::Quit, ); if theme != 0 { - element.at(1).unwrap().set(); + element.at(0).unwrap().set(); }; element } - -const COLORS: [[Color; 6]; 2] = [ - [ - Color::from_hex(0xfdf6e3), - Color::from_hex(0x586e75), - Color::from_hex(0xb58900), - Color::from_hex(0xeee8d5), - Color::from_hex(0xcb4b16), - Color::from_hex(0xdc322f), - ], - [ - Color::from_hex(0x002b36), - Color::from_hex(0x93a1a1), - Color::from_hex(0x268bd2), - Color::from_hex(0x073642), - Color::from_hex(0x6c71c4), - Color::from_hex(0xd33682), - ], -]; -const BUTTONS: [[&str; 4]; 5] = [ - ["CE", "C", "%", "/"], - ["7", "8", "9", "x"], - ["4", "5", "6", "-"], - ["1", "2", "3", "+"], - ["0", ".", "@<-", "="], -]; -const PAD: i32 = 10; -const HEIGHT: i32 = PAD * 3; -const NAME: &str = "FlCalculator"; -const PATH: &str = "/.config"; diff --git a/demos/calculator/src/model/mod.rs b/demos/calculator/src/model/mod.rs new file mode 100644 index 0000000..634d460 --- /dev/null +++ b/demos/calculator/src/model/mod.rs @@ -0,0 +1,98 @@ +use { + serde::{Deserialize, Serialize}, + std::{env, fs}, +}; + +#[derive(Deserialize, Serialize, Clone)] +pub struct Model { + pub prev: f64, + pub operation: String, + pub current: String, + pub output: String, + pub theme: bool, +} + +impl Model { + pub fn default() -> Self { + if let Ok(value) = fs::read(file()) { + if let Ok(value) = rmp_serde::from_slice::(&value) { + return value; + } + }; + Self { + prev: 0f64, + operation: String::new(), + current: String::from("0"), + output: String::new(), + theme: false, + } + } + pub fn save(&mut self) { + fs::write(file(), rmp_serde::to_vec(&self).unwrap()).unwrap(); + } + pub fn click(&mut self, value: &str) { + match value { + "/" | "x" | "+" | "-" | "%" => { + if self.current != "0" { + if self.operation.is_empty() { + self.prev = self.current.parse().unwrap(); + } else { + self.equil(); + } + self.output.push_str(&format!("{} {}", self.prev, value)); + self.operation = value.to_string(); + self.current = String::from("0"); + } + } + "=" => { + if !self.operation.is_empty() { + self.equil(); + self.operation.clear(); + } + } + "CE" => { + self.output.clear(); + self.operation.clear(); + self.current = String::from("0"); + self.prev = 0f64; + } + "@<-" => { + let label = self.current.clone(); + self.current = if label.len() > 1 { + String::from(&label[..label.len() - 1]) + } else { + String::from("0") + }; + } + "C" => self.current = String::from("0"), + "." => { + if !self.current.contains('.') { + self.current.push('.'); + } + } + _ => { + if self.current == "0" { + self.current.clear(); + } + self.current = self.current.clone() + value; + } + }; + } + fn equil(&mut self) { + self.output.push_str(&format!(" {}\n", self.current)); + let current: f64 = self.current.parse().unwrap(); + self.prev = match self.operation.as_str() { + "/" => self.prev / current, + "x" => self.prev * current, + "+" => self.prev + current, + "-" => self.prev - current, + _ => self.prev / 100.0 * current, + }; + self.output.push_str(&format!(" = {}\n", self.prev)); + self.current = String::from("0"); + } +} + +fn file() -> String { + env::var("HOME").unwrap() + "/.config/" + crate::NAME +} diff --git a/demos/csv/README.md b/demos/csv/README.md new file mode 100644 index 0000000..8a1eae3 --- /dev/null +++ b/demos/csv/README.md @@ -0,0 +1,6 @@ +# CSV + +Custom drawing of CSV data. Uses CSV and Serde. + +![img](https://github.com/fltk-rs/demos/blob/master/csv/assets/csv.gif) + diff --git a/demos/csv/assets/flcsv.png b/demos/csv/assets/flcsv.png deleted file mode 100644 index 7b71517..0000000 Binary files a/demos/csv/assets/flcsv.png and /dev/null differ diff --git a/demos/csv/assets/flerrands.gif b/demos/csv/assets/flerrands.gif deleted file mode 100644 index cc09ebc..0000000 Binary files a/demos/csv/assets/flerrands.gif and /dev/null differ diff --git a/demos/csv/src/main.rs b/demos/csv/src/main.rs index b8d331c..6117e54 100644 --- a/demos/csv/src/main.rs +++ b/demos/csv/src/main.rs @@ -4,13 +4,13 @@ mod model; use { flemish::{ - app, browser::{Browser, BrowserType}, button::Button, color_themes, draw, enums::{Color, FrameType}, frame::Frame, group::Flex, + image::SvgImage, prelude::*, OnEvent, Sandbox, Settings, }, @@ -25,10 +25,10 @@ const WIDTH: i32 = HEIGHT * 3; pub fn main() { Model::new().run(Settings { size: (640, 360), - resizable: false, - ignore_esc_close: true, + resizable: true, + xclass: Some(String::from(NAME)), + icon: Some(SvgImage::from_data(include_str!("../../assets/logo.svg")).unwrap()), color_map: Some(color_themes::DARK_THEME), - scheme: Some(app::Scheme::Base), ..Default::default() }) } diff --git a/demos/csv/src/model/mod.rs b/demos/csv/src/model/mod.rs index 36b7982..b311fb3 100644 --- a/demos/csv/src/model/mod.rs +++ b/demos/csv/src/model/mod.rs @@ -20,6 +20,8 @@ pub struct Price { _volume: usize, } +const DIR: &str = "../assets/historical_data"; + #[derive(Debug, Clone)] pub struct Model { pub cash: HashMap>, @@ -38,7 +40,7 @@ impl Model { } } pub fn init(&mut self) { - for file in std::fs::read_dir("assets/historical_data").unwrap() { + for file in std::fs::read_dir(DIR).unwrap() { let entry = file.unwrap().file_name().into_string().unwrap(); if entry.ends_with(".csv") { self.temp @@ -50,7 +52,7 @@ impl Model { pub fn choice(&mut self, curr: usize) { if self.cash.contains_key(&self.temp[curr]) { self.curr = curr; - } else if let Ok(data) = fs::read(format!("assets/historical_data/{}.csv", self.temp[curr])) + } else if let Ok(data) = fs::read(format!("{DIR}/{}.csv", self.temp[curr])) { let mut prices: Vec = Vec::new(); for result in Reader::from_reader(data.as_slice()).deserialize() { 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..28c0d9c --- /dev/null +++ b/demos/dialect/src/main.rs @@ -0,0 +1,417 @@ +#![forbid(unsafe_code)] + +mod model; + +use { + flemish::{ + app, + button::Button, + color_themes, + dialog::{alert_default, FileChooser, FileChooserType}, + enums::{Align, Color, Cursor, Event, Font, FrameType, Shortcut}, + frame::Frame, + image::SvgImage, + group::{Flex, FlexType, Wizard}, + menu::{Choice, MenuButton, MenuButtonType, MenuFlag}, + misc::HelpView, + prelude::*, + text::{TextBuffer, TextDisplay, TextEditor, WrapMode}, + valuator::{Counter, CounterType}, + OnEvent, OnMenuEvent, Sandbox, Settings, + }, + model::Model, + std::{env, process::Command, thread}, +}; + +const SPINNER: Event = Event::from_i32(405); +const NAME: &str = "FlDialect"; +const PAD: i32 = 10; +const HEIGHT: i32 = PAD * 3; +const WIDTH: i32 = 105; + +fn main() { + if crate::once() { + Model::new().run(Settings { + resizable: true, + size: (360, 640), + xclass: Some(String::from(NAME)), + icon: Some(SvgImage::from_data(include_str!("../../assets/logo.svg")).unwrap()), + color_map: Some(color_themes::DARK_THEME), + on_close_fn: Some(Box::new(move |_| { + if app::event() == Event::Close { + let (s, _) = app::channel::(); + s.send(Message::Quit); + } + })), + ..Default::default() + }); + } +} + +#[derive(Clone)] +pub enum Message { + From(i32), + To(i32), + Size(i32), + Font(i32), + Page(i32), + Source(String), + Switch, + Click, + 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 { + Model::default() + } + + fn view(&mut self) { + let mut wizard = Wizard::default_fill(); + { + let mut page = Flex::default_fill().column(); + { + let mut header = Flex::default(); + { + header.fixed(&crate::menu(), HEIGHT); + Frame::default(); + let lang = self + .lang + .iter() + .map(|x| x["name"].clone()) + .collect::>() + .join("|"); + header.fixed( + &crate::choice("From", &lang, self.from) + .clone() + .on_event(move |choice| Message::From(choice.value())), + WIDTH, + ); + crate::button("Switch", "@#refresh", &mut header) + .on_event(move |_| Message::Switch); + header.fixed( + &crate::choice("To", &lang, self.to) + .clone() + .on_event(move |choice| Message::To(choice.value())), + WIDTH, + ); + Frame::default(); + crate::button("Translate", "@#circle", &mut header) + .on_event(move |_| Message::Click); + } + header.end(); + header.set_pad(PAD); + page.fixed(&header, HEIGHT); + let mut hero = Flex::default_fill(); + { + crate::texteditor("Source", &self.source, self.font, self.size) + .on_event(move |text| Message::Source(text.buffer().unwrap().text())); + Frame::default(); + crate::textdisplay("Target", &self.target, self.font, self.size); + } + hero.end(); + hero.set_pad(0); + crate::orientation(&mut hero); + hero.handle(crate::resize); + } + page.end(); + page.set_pad(PAD); + page.set_margin(PAD); + page.set_frame(FrameType::FlatBox); + let mut page = Flex::default_fill(); + { + crate::info(); + } + page.end(); + page.set_margin(PAD); + page.handle(crate::back); + let mut page = Flex::default_fill(); + { + Frame::default(); + let mut right = Flex::default_fill().column(); + { + right.fixed( + &crate::choice("Font", &app::fonts().join("|"), self.font) + .with_label("Font") + .clone() + .on_event(move |choice| Message::Font(choice.value())), + HEIGHT, + ); + right.fixed( + &crate::counter("Size", self.size as f64) + .with_label("Size") + .clone() + .on_event(move |counter| Message::Size(counter.value() as i32)), + HEIGHT, + ); + } + right.end(); + right.set_pad(PAD); + } + page.end(); + page.set_margin(PAD); + page.handle(crate::back); + } + wizard.end(); + wizard.set_current_widget(&wizard.child(self.page).unwrap()); + } + + fn update(&mut self, message: Message) { + match message { + Message::Page(value) => self.page = value, + Message::Quit => { + self.save(); + 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::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.open(&file); + }; + }; + } + 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) { + self.target(&file) + }; + }; + } else { + alert_default("Target is empty."); + }; + } + Message::Click => { + 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) -> Counter { + let mut element = Counter::default() + .with_type(CounterType::Simple) + .with_align(Align::Left); + element.set_tooltip(tooltip); + element.set_range(14_f64, 22_f64); + element.set_precision(0); + element.set_value(value); + element +} + +fn info() { + let (r, g, b) = Color::from_hex(0x2aa198).to_rgb(); + app::set_color(Color::Blue, r, g, b); + let mut help = HelpView::default(); + help.set_value(include_str!("../README.md")); + help.set_text_size(16); +} + +fn back(flex: &mut Flex, event: Event) -> bool { + match event { + Event::Push => match app::event_mouse_button() { + app::MouseButton::Right => { + MenuButton::default() + .with_type(MenuButtonType::Popup3) + .clone() + .on_item_event( + "@<- &Back", + Shortcut::None, + MenuFlag::Normal, + move |_| Message::Page(0), + ) + .popup(); + true + } + _ => false, + }, + Event::Enter => { + flex.window().unwrap().set_cursor(Cursor::Hand); + true + } + Event::Leave => { + flex.window().unwrap().set_cursor(Cursor::Arrow); + true + } + _ => false, + } +} + +fn choice(tooltip: &str, choice: &str, value: i32) -> Choice { + let mut element = Choice::default(); + element.set_tooltip(tooltip); + element.add_choice(choice); + element.set_value(value); + element +} + +fn resize(flex: &mut Flex, event: Event) -> bool { + if event == Event::Resize { + crate::orientation(flex); + true + } else { + false + } +} + +fn orientation(flex: &mut Flex) { + flex.set_type(match flex.width() < flex.height() { + true => FlexType::Column, + false => FlexType::Row, + }); + flex.fixed(&flex.child(1).unwrap(), PAD); +} + +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() -> MenuButton { + MenuButton::default() + .clone() + .on_item_event( + "@#fileopen &Open...", + Shortcut::Ctrl | 'o', + MenuFlag::Normal, + move |_| Message::Open, + ) + .on_item_event( + "@#filesaveas &Save as...", + Shortcut::Ctrl | 's', + MenuFlag::Normal, + move |_| Message::Save, + ) + .on_item_event( + "@#circle T&ranslate", + Shortcut::Ctrl | 'r', + MenuFlag::Normal, + move |_| Message::Click, + ) + .on_item_event( + "@#search &Info", + Shortcut::Ctrl | 'i', + MenuFlag::Normal, + move |_| Message::Page(1), + ) + .on_item_event( + "@#menu Se&ttings", + Shortcut::Ctrl | 't', + MenuFlag::Normal, + move |_| Message::Page(2), + ) + .on_item_event( + "@#1+ &Quit", + Shortcut::Ctrl | 'q', + MenuFlag::Normal, + move |_| Message::Quit, + ) +} + +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 + } +} diff --git a/demos/dialect/src/model/mod.rs b/demos/dialect/src/model/mod.rs new file mode 100644 index 0000000..3f6153a --- /dev/null +++ b/demos/dialect/src/model/mod.rs @@ -0,0 +1,90 @@ +use { + serde::{Deserialize, Serialize}, + std::{collections::HashMap, env, 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::from([HashMap::from([( + String::from("name"), + String::from("Not connect"), + )])]) + } + } + 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 page: i32, + pub hero: bool, + 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() -> Self { + if let Ok(value) = fs::read(file()) { + if let Ok(value) = rmp_serde::from_slice::(&value) { + return value; + } + }; + Self { + hero: true, + page: 0, + from: 0, + to: 0, + font: 1, + size: 14, + source: String::from("Source"), + target: String::from("Target"), + lang: Lang::init(), + } + } + 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) { + fs::write(file(), rmp_serde::to_vec(&self).unwrap()).unwrap(); + } + pub fn open(&mut self, file: &str) { + self.source = fs::read_to_string(file).unwrap(); + } + pub fn target(&mut self, file: &str) { + fs::write(file, self.target.as_bytes()).unwrap(); + } +} + +fn file() -> String { + env::var("HOME").unwrap() + "/.config/" + crate::NAME +} diff --git a/demos/flightbooker/src/main.rs b/demos/flightbooker/src/main.rs index b1fec32..9e323b9 100644 --- a/demos/flightbooker/src/main.rs +++ b/demos/flightbooker/src/main.rs @@ -4,7 +4,7 @@ mod model; use { flemish::{ - app, button::Button, color_themes, dialog::alert_default, enums::FrameType, frame::Frame, + button::Button, color_themes, dialog::alert_default, enums::FrameType, frame::Frame, group::Flex, input::Input, menu::Choice, prelude::*, OnEvent, Sandbox, Settings, }, model::Model, @@ -13,10 +13,7 @@ use { pub fn main() { Model::new().run(Settings { size: (640, 360), - resizable: false, - ignore_esc_close: true, color_map: Some(color_themes::DARK_THEME), - scheme: Some(app::Scheme::Base), ..Default::default() }) } diff --git a/demos/resters/Cargo.toml b/demos/resters/Cargo.toml new file mode 100644 index 0000000..1b79f1b --- /dev/null +++ b/demos/resters/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "flresters" +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 = "../../" } +serde = { version="1.0", features = ["derive"] } +rmp-serde = { version="1.1" } +ureq = { version = "2.9", features = ["json"] } +serde_json = "1" +json-tools = "1.1" diff --git a/demos/resters/README.md b/demos/resters/README.md new file mode 100644 index 0000000..ebf33e0 --- /dev/null +++ b/demos/resters/README.md @@ -0,0 +1,5 @@ +# Rester demo + +It's just resters. + +![img](https://github.com/fltk-rs/demos/tree/master/flresters/assets/flresters.gif) diff --git a/examples/flresters.rs b/demos/resters/src/main.rs similarity index 54% rename from examples/flresters.rs rename to demos/resters/src/main.rs index 3ac7b5f..fea26ec 100644 --- a/examples/flresters.rs +++ b/demos/resters/src/main.rs @@ -1,84 +1,73 @@ #![forbid(unsafe_code)] +mod model; + use { flemish::{ app, button::Button, color_themes, - enums::{Align, Color, Font, FrameType}, + enums::{Color, Event, Font, FrameType}, frame::Frame, - misc::InputChoice, group::Flex, menu::Choice, + image::SvgImage, + misc::{InputChoice, Progress}, prelude::*, text::{StyleTableEntry, TextBuffer, TextDisplay, WrapMode}, OnEvent, Sandbox, Settings, }, - std::{process::Command, thread}, + json_tools::{Buffer, BufferType, Lexer, Span, TokenType}, + model::Model, }; -pub fn main() { +const NAME: &str = "FlResters"; + +fn main() { Model::new().run(Settings { + resizable: true, size: (640, 360), - resizable: false, - ignore_esc_close: true, + xclass: Some(String::from(NAME)), + icon: Some(SvgImage::from_data(include_str!("../../assets/logo.svg")).unwrap()), color_map: Some(color_themes::DARK_THEME), - scheme: Some(app::Scheme::Base), ..Default::default() }) } #[derive(Clone)] -struct Model { - method: u8, - url: String, - responce: String, - status: String, -} - -#[derive(Clone)] -enum Message { +pub enum Message { Method(u8), Url(String), - Request, + Thread, } impl Sandbox for Model { type Message = Message; fn new() -> Self { - Self { - method: 0, - url: String::new(), - responce: String::new(), - status: String::new(), - } + Model::default() } fn title(&self) -> String { - String::from("flResters") + String::from(NAME) } 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())); - crate::button(&mut header).on_event(move |_| Message::Request); + crate::button(&mut header).on_event(move |_| Message::Thread); header.end(); crate::text(&self.responce); 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::progress().with_label(&self.status), WIDTH); footer.end(); page.end(); { @@ -95,12 +84,14 @@ impl Sandbox for Model { match message { Message::Method(value) => self.method = value, Message::Url(value) => self.url = value, - Message::Request => { - let url = match self.url.starts_with("https://") { - true => self.url.clone(), - false => String::from("https://") + &self.url, - }; - let handler = thread::spawn(move || -> (bool, String) { crate::curl(url) }); + Message::Thread => { + let clone = self.clone(); + let handler = std::thread::spawn(move || -> (bool, String) { clone.click() }); + while !handler.is_finished() { + app::wait(); + app::handle_main(SPINNER).unwrap(); + app::sleep(0.02); + } if let Ok((status, check)) = handler.join() { self.status = match status { true => "OK", @@ -114,6 +105,26 @@ impl Sandbox for Model { } } +fn progress() -> Progress { + const MAX: u8 = 120; + let mut element = Progress::default(); + element.set_maximum((MAX / 4 * 3) as f64); + element.set_value(element.minimum()); + element.handle(move |progress, event| { + if event == crate::SPINNER { + progress.set_value(if progress.value() == (MAX - 1) as f64 { + progress.minimum() + } else { + progress.value() + 1f64 + }); + true + } else { + false + } + }); + element +} + fn choice(value: i32, flex: &mut Flex) -> Choice { let mut element = Choice::default(); element.add_choice("GET|POST"); @@ -123,6 +134,8 @@ fn choice(value: i32, flex: &mut Flex) -> Choice { } fn text(value: &str) { + let mut buffer = TextBuffer::default(); + buffer.set_text(&crate::fill_style_buffer(value)); let styles: Vec = [0xdc322f, 0x268bd2, 0x859900] .into_iter() .map(|color| StyleTableEntry { @@ -135,7 +148,7 @@ fn text(value: &str) { element.wrap_mode(WrapMode::AtBounds, 0); element.set_buffer(TextBuffer::default()); element.set_color(Color::from_hex(0x002b36)); - element.set_highlight_data(TextBuffer::default(), styles); + element.set_highlight_data(buffer, styles); element.buffer().unwrap().set_text(value); } @@ -151,27 +164,38 @@ 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:\/\/lingva.thedaviddelta.com\/api\/v1\/en\/de\/mother"#); element.add(r#"https:\/\/ipinfo.io\/json"#); element.set_value(value); element } -fn curl(url: String) -> (bool, String) { - 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(), - ) +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 new file mode 100644 index 0000000..5f9c41b --- /dev/null +++ b/demos/resters/src/model/mod.rs @@ -0,0 +1,40 @@ +#[derive(Clone)] +pub struct Model { + pub method: u8, + pub url: String, + pub responce: String, + pub status: String, +} + +impl Model { + pub fn default() -> Self { + Self { + method: 0, + url: String::from(r#"https://ipinfo.io/json"#), + responce: String::new(), + status: String::new(), + } + } + pub fn click(&self) -> (bool, String) { + let url = match self.url.starts_with("https://") { + true => self.url.clone(), + false => String::from("https://") + &self.url, + }; + if let Ok(response) = match self.method { + 0 => ureq::get(&url), + 1 => ureq::post(&url), + _ => unreachable!(), + } + .call() + { + let body = response.into_string().unwrap(); + if let Ok(json) = serde_json::from_str::(&body) { + (true, serde_json::to_string_pretty(&json).unwrap()) + } else { + (true, body) + } + } else { + (false, String::from("Error")) + } + } +} diff --git a/demos/sudokusolver/Cargo.toml b/demos/sudokusolver/Cargo.toml new file mode 100644 index 0000000..9caf614 --- /dev/null +++ b/demos/sudokusolver/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sudokusolver" +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 = "../../" } diff --git a/demos/sudokusolver/README.md b/demos/sudokusolver/README.md new file mode 100644 index 0000000..4595319 --- /dev/null +++ b/demos/sudokusolver/README.md @@ -0,0 +1,5 @@ +# Sudokusolver + +It's implementation that [program](https://github.com/andersjoern/sudokusolver/). + +![img](https://github.com/andersjoern/sudokusolver/blob/master/screenshots/SudokuSolver1.png) diff --git a/demos/sudokusolver/src/main.rs b/demos/sudokusolver/src/main.rs new file mode 100644 index 0000000..d6f46bf --- /dev/null +++ b/demos/sudokusolver/src/main.rs @@ -0,0 +1,118 @@ +#![forbid(unsafe_code)] + +mod model; + +use { + flemish::{ + app, + button::Button, + color_themes, + enums::{Event, FrameType}, + frame::Frame, + group::Flex, + menu::{MenuButton, MenuButtonType}, + prelude::*, + OnEvent, Sandbox, Settings, + }, + model::Model, +}; + +pub fn main() { + Model::new().run(Settings { + size: (310, 350), + color_map: Some(color_themes::DARK_THEME), + ..Default::default() + }) +} + +const NAME: &str = "Sudoku solver"; +const PAD: i32 = 10; +const HEIGHT: i32 = PAD * 3; + +#[derive(Clone)] +pub enum Message { + Click((usize, usize, i32)), + Solve, + Clear, +} + +impl Sandbox for Model { + type Message = Message; + + fn title(&self) -> String { + String::from(NAME) + } + + fn new() -> Self { + Self::default() + } + + fn view(&mut self) { + let mut page = Flex::default_fill().column(); + let mut hero = Flex::default().column(); + for (row, record) in self.grid.iter().enumerate() { + if row > 0 && row % 3 == 0 { + hero.fixed(&Frame::default(), PAD); + }; + let mut flex = Flex::default(); + for (col, cell) in record.iter().enumerate() { + if col > 0 && col % 3 == 0 { + flex.fixed(&Frame::default(), PAD); + }; + flex.fixed(&crate::frame(row, col, *cell), HEIGHT); + } + flex.end(); + flex.set_pad(0); + } + hero.end(); + hero.set_pad(0); + let mut footer = Flex::default(); + { + Button::default() + .with_label("Solve") + .on_event(move |_| Message::Solve); + Button::default() + .with_label("Clear") + .on_event(move |_| Message::Clear); + } + footer.end(); + footer.set_pad(PAD); + page.end(); + page.fixed(&footer, HEIGHT); + page.set_margin(PAD); + page.set_pad(PAD); + } + + fn update(&mut self, message: Message) { + match message { + Message::Click((row, col, value)) => self.grid[row][col] = value, + Message::Solve => self.answer(), + Message::Clear => self.clear(), + } + } +} + +fn frame(row: usize, col: usize, value: i32) -> Frame { + let mut element = Frame::default(); + if value > 0 { + element.set_label(&value.to_string()); + }; + element.set_frame(FrameType::DownBox); + element.set_label_size(18); + element.handle(move |_, event| match event { + Event::Push => match app::event_mouse_button() { + app::MouseButton::Right => { + let mut menu = MenuButton::default().with_type(MenuButtonType::Popup3); + menu.set_text_size(18); + menu.add_choice(" 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 "); + menu.clone() + .on_event(move |choice| Message::Click((row, col, choice.value() + 1))) + .popup(); + true + } + _ => false, + }, + _ => false, + }); + element +} diff --git a/demos/sudokusolver/src/model/mod.rs b/demos/sudokusolver/src/model/mod.rs new file mode 100644 index 0000000..af08c94 --- /dev/null +++ b/demos/sudokusolver/src/model/mod.rs @@ -0,0 +1,110 @@ +pub struct Model { + pub grid: [[i32; 9]; 9], +} + +impl Model { + pub fn default() -> Self { + Self { grid: [[0; 9]; 9] } + } + pub fn clear(&mut self) { + self.grid = [[0; 9]; 9]; + } + fn solvable(&mut self) -> bool { + let mut items: [i32; 9]; + + for row in self.grid { + items = [0; 9]; + for value in row { + if value > 0 && value < 10 { + items[(value - 1) as usize] += 1; + } + } + if items.iter().any(|&n| n > 1) { + return false; + } + } + + for i in 0..9 { + items = [0; 9]; + for row in self.grid { + if row[i] > 0 && row[i] < 10 { + items[(row[i] - 1) as usize] += 1; + } + } + if items.iter().any(|&n| n > 1) { + return false; + } + } + + for &x in [0, 3, 6].iter() { + for &y in [0, 3, 6].iter() { + items = [0; 9]; + for i in 0..3 { + for j in 0..3 { + if self.grid[y + i][x + j] > 0 && self.grid[y + i][x + j] < 10 { + items[(self.grid[y + i][x + j] - 1) as usize] += 1; + } + } + } + if items.iter().any(|&n| n > 1) { + return false; + } + } + } + true + } + fn possible(&self, y: usize, x: usize, number: i32) -> bool { + if self.grid[y].iter().any(|&n| n == number) { + return false; + } + + if self.grid.iter().any(|n| n[x] == number) { + return false; + } + + let x0: usize = (x / 3) * 3; + let y0: usize = (y / 3) * 3; + + for i in 0..3 { + for j in 0..3 { + if self.grid[y0 + i][x0 + j] == number { + return false; + } + } + } + true + } + fn find_next_cell2fill(&self) -> (usize, usize) { + for (x, row) in self.grid.iter().enumerate() { + for (y, &val) in row.iter().enumerate() { + if val == 0 { + return (x, y); + } + } + } + (99, 99) + } + fn solve(&mut self) -> bool { + let (i, j) = self.find_next_cell2fill(); + if i == 99 { + return true; + } + for e in 1..10 { + if self.possible(i, j, e) { + self.grid[i][j] = e; + if self.solve() { + return true; + } + self.grid[i][j] = 0; + } + } + false + } + pub fn answer(&mut self) { + if self.solvable() { + self.solve(); + } else { + self.clear(); + } + } +} diff --git a/demos/text/Cargo.toml b/demos/text/Cargo.toml new file mode 100644 index 0000000..f1554a0 --- /dev/null +++ b/demos/text/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "texteditor" +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 = "../../" } diff --git a/demos/text/README.md b/demos/text/README.md new file mode 100644 index 0000000..604b0b6 --- /dev/null +++ b/demos/text/README.md @@ -0,0 +1,5 @@ +# Base64 demo + +It's implementation that [program](https://github.com/andersjoern/b64converter/). + +![img](https://github.com/andersjoern/b64converter/blob/master/screenshots/encode_decode.png) diff --git a/demos/text/src/main.rs b/demos/text/src/main.rs new file mode 100644 index 0000000..2cee65f --- /dev/null +++ b/demos/text/src/main.rs @@ -0,0 +1,137 @@ +mod model; + +use { + flemish::{ + app, + color_themes, + enums::{Color,Shortcut}, + image::SvgImage, + group::Flex, + dialog::{FileChooser,FileChooserType}, + menu::{MenuBar, MenuFlag}, + prelude::*, + text::{TextBuffer, TextEditor, WrapMode}, + OnEvent, OnMenuEvent, Sandbox, Settings, + }, + model::Model, + std::env, +}; + +const PAD: i32 = 10; +const HEIGHT: i32 = PAD * 3; +const NAME: &str = "FlText"; + +#[derive(Clone)] +pub enum Message { + Open, + Save, + Change, +} + +fn main() { + Model::new().run(Settings { + resizable: true, + size: (360, 640), + xclass: Some(String::from(NAME)), + icon: Some(SvgImage::from_data(include_str!("../../assets/logo.svg")).unwrap()), + color_map: Some(color_themes::DARK_THEME), + ..Default::default() + }) +} + +impl Sandbox for Model { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from(NAME) + } + + fn view(&mut self) { + let mut page = Flex::default_fill().column(); + { + page.fixed(&crate::menu(), HEIGHT); + crate::texteditor(&self.text).on_event(move |_| Message::Change); + } + page.end(); + page.set_pad(PAD); + page.set_margin(PAD); + } + + fn update(&mut self, message: Message) { + match message { + Message::Change => self.save = false, + 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.path = file; + self.open(); + self.save = true; + }; + }; + } + Message::Save => { + if !self.text.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) { + self.path = file + }; + }; + self.save(); + } + } + } + } +} + +fn menu() -> MenuBar { + MenuBar::default() + .clone() + .on_item_event( + "@#fileopen &Open...", + Shortcut::Ctrl | 'o', + MenuFlag::Normal, + move |_| Message::Open, + ) + .on_item_event( + "@#filesaveas &Save as...", + Shortcut::Ctrl | 's', + MenuFlag::Normal, + move |_| Message::Save, + ) +} + +fn texteditor(value: &str) -> TextEditor { + let mut element = TextEditor::default(); + element.set_linenumber_width(0); + 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 +} + diff --git a/demos/text/src/model/mod.rs b/demos/text/src/model/mod.rs new file mode 100644 index 0000000..876d4b9 --- /dev/null +++ b/demos/text/src/model/mod.rs @@ -0,0 +1,25 @@ +use std::fs; + +#[derive(Debug)] +pub struct Model { + pub save: bool, + pub text: String, + pub path: String, +} + +impl Model { + pub fn default() -> Self { + Self { + save: false, + text: String::new(), + path: String::new(), + } + } + pub fn open(&mut self) { + self.text = fs::read_to_string(&self.path).unwrap(); + } + pub fn save(&mut self) { + fs::write(&self.path, self.text.as_bytes()).unwrap(); + self.save = true; + } +} diff --git a/demos/fltodo/Cargo.toml b/demos/todo/Cargo.toml similarity index 100% rename from demos/fltodo/Cargo.toml rename to demos/todo/Cargo.toml diff --git a/demos/fltodo/assets/fltodo.gif b/demos/todo/assets/fltodo.gif similarity index 100% rename from demos/fltodo/assets/fltodo.gif rename to demos/todo/assets/fltodo.gif diff --git a/demos/fltodo/src/main.rs b/demos/todo/src/main.rs similarity index 94% rename from demos/fltodo/src/main.rs rename to demos/todo/src/main.rs index 81c378b..080b4fe 100644 --- a/demos/fltodo/src/main.rs +++ b/demos/todo/src/main.rs @@ -24,9 +24,7 @@ pub fn main() { Model::new().run(Settings { size: (360, 640), resizable: false, - ignore_esc_close: true, color_map: Some(color_themes::DARK_THEME), - scheme: Some(app::Scheme::Base), ..Default::default() }) } @@ -52,7 +50,7 @@ impl Sandbox for Model { } fn new() -> Self { - let file = app::GlobalState::::get().with(move |model| model.clone()); + let file = app::GlobalState::::get().with(move |file| file.clone()); let default = Self { tasks: Vec::new() }; if let Ok(value) = fs::read(file) { if let Ok(value) = rmp_serde::from_slice(&value) { @@ -124,8 +122,7 @@ impl Sandbox for Model { fn update(&mut self, message: Message) { match message { Message::Quit => { - let file = app::GlobalState::::get().with(move |model| model.clone()); - self.save(file); + self.save(app::GlobalState::::get().with(move |file| file.clone())); app::quit(); } Message::Delete(idx) => { diff --git a/demos/fltodo/src/model/mod.rs b/demos/todo/src/model/mod.rs similarity index 100% rename from demos/fltodo/src/model/mod.rs rename to demos/todo/src/model/mod.rs diff --git a/examples/checkbutton.rs b/examples/checkbutton.rs index a04ee71..7a9f308 100644 --- a/examples/checkbutton.rs +++ b/examples/checkbutton.rs @@ -8,10 +8,7 @@ use flemish::{ pub fn main() { Model::new().run(Settings { size: (640, 360), - resizable: false, - ignore_esc_close: true, color_map: Some(color_themes::DARK_THEME), - scheme: Some(app::Scheme::Base), ..Default::default() }) } diff --git a/examples/counter.rs b/examples/counter.rs index 9f4c55e..daee415 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -8,10 +8,7 @@ use flemish::{ pub fn main() { Model::new().run(Settings { size: (640, 360), - resizable: false, - ignore_esc_close: true, color_map: Some(color_themes::DARK_THEME), - scheme: Some(app::Scheme::Base), ..Default::default() }) } diff --git a/examples/crud.rs b/examples/crud.rs index 7187a5e..deb18e0 100644 --- a/examples/crud.rs +++ b/examples/crud.rs @@ -15,10 +15,7 @@ use flemish::{ pub fn main() { Model::new().run(Settings { size: (640, 360), - resizable: false, - ignore_esc_close: true, color_map: Some(color_themes::DARK_THEME), - scheme: Some(app::Scheme::Base), ..Default::default() }) } diff --git a/examples/fldialect.rs b/examples/fldialect.rs deleted file mode 100644 index 403d28e..0000000 --- a/examples/fldialect.rs +++ /dev/null @@ -1,473 +0,0 @@ -#![forbid(unsafe_code)] - -use { - flemish::{ - app, - button::{Button, ButtonType}, - color_themes, - dialog::{alert_default, FileChooser, FileChooserType, HelpDialog}, - enums::{Color, Font, FrameType, Shortcut}, - frame::Frame, - group::Flex, - menu::{Choice, MenuButton, MenuFlag}, - prelude::*, - text::{TextBuffer, TextEditor, WrapMode}, - valuator::{Counter, CounterType, Dial}, - OnEvent, OnMenuEvent, Sandbox, Settings, - }, - std::{env, fs, path::Path, process::Command, thread}, -}; - -fn main() { - if crate::once() { - app::GlobalState::::new(env::var("HOME").unwrap() + PATH + NAME); - let mut app = Model::new(); - app.run(Settings { - size: (app.width, app.height), - pos: (app.vertical, app.horizontal), - ignore_esc_close: true, - resizable: false, - color_map: Some(color_themes::DARK_THEME), - scheme: Some(app::Scheme::Base), - ..Default::default() - }); - } -} - -struct Model { - width: i32, - height: i32, - vertical: i32, - horizontal: i32, - from: u8, - to: u8, - speak: bool, - font: u8, - size: u8, - source: String, - target: String, - lang: Vec, -} - -#[derive(Clone)] -enum Message { - Switch, - From(u8), - To(u8), - Speak(bool), - Source(String), - Size(u8), - Font(u8), - Info, - Translate, - Open, - Save, - Quit, -} - -impl Sandbox for Model { - type Message = Message; - - fn title(&self) -> String { - String::from(NAME) - } - - fn new() -> Self { - let file = app::GlobalState::::get().with(move |model| model.clone()); - let params: Vec = if Path::new(&file).exists() { - if let Ok(value) = fs::read(&file) { - if value.len() == DEFAULT.len() { - value - } else { - fs::remove_file(&file).unwrap(); - Vec::from(DEFAULT) - } - } else { - Vec::from(DEFAULT) - } - } else { - Vec::from(DEFAULT) - }; - let (w, h) = app::screen_size(); - let width = params[0] as i32 * U8 + params[1] as i32; - let height = params[2] as i32 * U8 + params[3] as i32; - let mut lang = crate::list(); - lang.sort(); - Self { - width, - height, - from: params[4], - to: params[5], - font: params[6], - size: params[7], - vertical: ((w - width as f64) / 2_f64) as i32, - horizontal: ((h - height as f64) / 2_f64) as i32, - speak: false, - source: String::new(), - target: String::new(), - lang, - } - } - - fn view(&mut self) { - let mut page = Flex::default_fill().column(); - { - let mut header = Flex::default(); - crate::menu(&mut header); - Frame::default(); - crate::choice("From", &self.lang.join("|"), self.from, &mut header) - .on_event(move |choice| Message::From(choice.value() as u8)); - crate::button("Switch", "@#refresh", &mut header).on_event(move |_| Message::Switch); - crate::choice("To", &self.lang.join("|"), self.to, &mut header) - .on_event(move |choice| Message::To(choice.value() as u8)); - Frame::default(); - let mut button = - crate::button("Speak", "@#<", &mut header).with_type(ButtonType::Toggle); - button.set(self.speak); - button.on_event(move |button| Message::Speak(button.value())); - header.end(); - header.set_pad(0); - page.fixed(&header, HEIGHT); - } - { - let mut hero = Flex::default_fill().column().with_id("HERO"); - crate::text("Source", &self.source, self.font, self.size) - .on_event(move |text| Message::Source(text.buffer().unwrap().text())); - hero.fixed(&Frame::default(), PAD); - crate::text("Target", &self.target, self.font, self.size); - hero.end(); - hero.set_pad(0); - hero.fixed(&hero.child(1).unwrap(), PAD); - } - { - let mut footer = Flex::default(); //FOOTER - crate::button("Open...", "@#fileopen", &mut footer).on_event(move |_| Message::Open); - Frame::default(); - crate::choice("Font", &app::fonts().join("|"), self.font, &mut footer) - .on_event(move |choice| Message::Font(choice.value() as u8)); - crate::button("Translate", "@#circle", &mut footer) - .on_event(move |_| Message::Translate); - crate::counter("Size", self.size as f64, &mut footer) - .with_type(CounterType::Simple) - .on_event(move |counter| Message::Size(counter.value() as u8)); - crate::dial(&mut footer); - Frame::default(); - crate::button("Save as...", "@#filesaveas", &mut footer) - .on_event(move |_| Message::Save); - 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); - let mut window = page.window().unwrap(); - window.set_xclass(NAME); - window.set_label(&format!( - "Translate from {} to {} - {NAME}", - self.lang[self.from as usize], self.lang[self.to as usize] - )); - } - } - - fn update(&mut self, message: Message) { - match message { - Message::Speak(value) => self.speak = value, - Message::From(value) => self.from = value, - Message::To(value) => self.to = value, - Message::Source(value) => self.source = value, - Message::Switch => { - let temp = self.from; - self.from = self.to; - self.to = temp; - } - Message::Font(value) => self.font = value, - Message::Size(value) => self.size = value, - Message::Open => self.open(), - Message::Save => self.save(), - Message::Translate => self.translate(), - Message::Info => crate::info(), - Message::Quit => self.quit(), - } - } -} - -impl Model { - fn open(&mut self) { - 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(); - }; - }; - } - fn save(&self) { - 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."); - }; - } - fn translate(&mut self) { - let mut button = app::widget_from_id::