From 3a003b413b4559af02c8ec64de634e8b2573951b Mon Sep 17 00:00:00 2001 From: "Artem V. Ageev" Date: Sun, 7 Apr 2024 20:19:55 +0300 Subject: [PATCH] add flmusic and more (#22) * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * small fix * add use-ninja * add use-ninja * add app = --- .github/workflows/rust.yml | 175 +++-- 7gui/Cargo.toml | 12 + 7gui/README.md | 4 + 7gui/src/components/cells.rs | 636 ++++++++++++++++++ 7gui/src/components/circles.rs | 246 +++++++ 7gui/src/components/counter.rs | 44 ++ 7gui/src/components/crud.rs | 106 +++ 7gui/src/components/flightbooker.rs | 96 +++ 7gui/src/components/mod.rs | 11 + 7gui/src/components/temperature.rs | 45 ++ 7gui/src/components/timer.rs | 43 ++ 7gui/src/main.rs | 97 +++ README.md | 161 +---- cairo/Cargo.toml | 4 +- cairo/src/main.rs | 3 +- cairo_shadow_button/Cargo.toml | 5 +- cairo_shadow_button/src/main.rs | 9 +- calendar/Cargo.toml | 6 +- calendar/README.md | 2 +- calendar/src/calendar.rs | 11 +- calendar/src/main.rs | 25 +- csv/Cargo.toml | 14 +- csv/README.md | 2 +- csv/{ => assets}/csv.gif | Bin csv/{ => assets}/ex.jpg | Bin csv/{ => assets}/historical_data/GME.csv | 0 csv/{ => assets}/historical_data/dlpn.csv | 0 csv/{ => assets}/historical_data/oil.csv | 0 csv/src/main.rs | 66 +- {egui-demo => egui}/Cargo.toml | 0 {egui-demo => egui}/README.md | 2 +- .../egui-demo.gif => egui/assets/egui.gif | Bin {egui-demo => egui/assets}/egui.jpg | Bin {egui-demo => egui}/src/main.rs | 26 +- femtovg/Cargo.toml | 8 +- femtovg/README.md | 2 +- femtovg/{ => assets}/ex.png | Bin femtovg/{ => assets}/femtovg.gif | Bin femtovg/src/main.rs | 41 +- ffmpeg/Cargo.toml | 2 +- ffmpeg/src/main.rs | 28 +- flcalculator/Cargo.toml | 4 +- flcalculator/src/main.rs | 43 +- fldialect/Cargo.toml | 4 +- fldialect/src/commands/mod.rs | 5 +- fldialect/src/components/footer.rs | 6 +- fldialect/src/constants/mod.rs | 13 +- fldialect/src/pages/mod.rs | 33 +- flmusic/Cargo.toml | 10 + flmusic/README.md | 2 + flmusic/assets/base.png | Bin 0 -> 23523 bytes flmusic/assets/base_dark.png | Bin 0 -> 24041 bytes flmusic/src/components/mod.rs | 252 +++++++ flmusic/src/main.rs | 53 ++ flpicture/Cargo.toml | 11 + flpicture/README.md | 2 + flpicture/assets/base.png | Bin 0 -> 21722 bytes flpicture/assets/base_dark.png | Bin 0 -> 23797 bytes flpicture/src/components/mod.rs | 220 ++++++ flpicture/src/main.rs | 40 ++ flresters/Cargo.toml | 18 + flresters/LICENSE | 19 + flresters/README.md | 2 + flresters/assets/base_dark_linux.png | Bin 0 -> 8860 bytes flresters/assets/full_dark_linux.png | Bin 0 -> 110806 bytes flresters/src/main.rs | 142 ++++ fltext/Cargo.toml | 30 + fltext/LICENSE | 21 + fltext/README.md | 34 + fltext/src/cbs.rs | 222 ++++++ fltext/src/dialogs.rs | 199 ++++++ fltext/src/fbr.rs | 128 ++++ fltext/src/gui.rs | 269 ++++++++ fltext/src/highlight/colors.rs | 9 + fltext/src/highlight/md.rs | 23 + fltext/src/highlight/mod.rs | 130 ++++ fltext/src/highlight/rust.rs | 36 + fltext/src/highlight/toml.rs | 24 + fltext/src/main.rs | 18 + fltext/src/state.rs | 143 ++++ fltext/src/utils.rs | 63 ++ framebuffer/Cargo.toml | 2 +- framebuffer/README.md | 2 +- framebuffer/{ => assets}/ex.jpg | Bin framebuffer/{ => assets}/framebuffer.gif | Bin framebuffer/src/main.rs | 16 +- glium/Cargo.toml | 4 +- glium/README.md | 2 +- glium/{ => assets}/ex.jpg | Bin glium/{ => assets}/glium.gif | Bin glium/src/main.rs | 65 +- glow/Cargo.toml | 8 +- glow/README.md | 2 +- glow/{ => assets}/ex.jpg | Bin glow/{ => assets}/glow.gif | Bin glow/src/main.rs | 10 +- glut/.gitignore | 0 glut/Cargo.toml | 6 +- glut/README.md | 2 +- glut/{ => assets}/ex.png | Bin glut/{ => assets}/glut.gif | Bin glut/src/main.rs | 31 +- glyphmap/Cargo.toml | 4 +- glyphmap/README.md | 4 +- glyphmap/{ => assets}/glyphmap.gif | Bin glyphmap/{ => assets}/image.jpg | Bin glyphmap/src/main.rs | 13 +- gst/Cargo.toml | 4 +- gst/src/main.rs | 19 +- image/Cargo.toml | 4 +- image/{ => assets}/fltk.png | Bin image/src/main.rs | 32 +- inner/Cargo.toml | 9 + inner/README.md | 3 + inner/src/main.rs | 32 + libmpv/Cargo.toml | 4 +- libmpv/src/main.rs | 15 +- libvlc/Cargo.toml | 2 +- libvlc/README.md | 2 +- libvlc/{ => assets}/ex.jpg | Bin libvlc/{ => assets}/video.mp4 | Bin libvlc/src/main.rs | 34 +- mpv/Cargo.toml | 2 +- mpv/src/main.rs | 27 +- musicplayer/Cargo.toml | 4 +- musicplayer/README.md | 2 +- musicplayer/{ => assets}/Alarm.mp3 | Bin musicplayer/{ => assets}/musicplayer.gif | Bin musicplayer/{ => assets}/musicplayer.png | Bin musicplayer/src/fancy_slider.rs | 2 +- musicplayer/src/main.rs | 16 +- musicplayer/src/power_button.rs | 17 +- opengl/Cargo.toml | 2 +- opengl/README.md | 4 +- opengl/{ => assets}/ex.jpg | Bin opengl/{ => assets}/opengl.gif | Bin opengl/src/main.rs | 19 +- pixels/Cargo.toml | 2 +- pixels/README.md | 2 +- pixels/{ => assets}/ex.jpg | Bin pixels/{ => assets}/pixels.gif | Bin plotters/Cargo.toml | 4 +- plotters/README.md | 2 +- plotters/{ => assets}/ex.jpg | Bin plotters/{ => assets}/plotters.gif | Bin plotters/src/main.rs | 43 +- raqote/Cargo.toml | 6 +- raqote/README.md | 2 +- raqote/{ => assets}/ex.jpg | Bin raqote/{ => assets}/raqote.gif | Bin raqote/src/main.rs | 29 +- rounded-svg/Cargo.toml | 4 +- rounded-svg/README.md | 9 +- rounded-svg/{ => assets}/rounded-svg.gif | Bin rounded-svg/src/main.rs | 17 +- speedy2d/Cargo.toml | 4 +- speedy2d/README.md | 2 +- speedy2d/{ => assets}/ex.jpg | Bin speedy2d/{ => assets}/speedy2d.gif | Bin speedy2d/src/main.rs | 37 +- systray/Cargo.toml | 4 +- systray/README.md | 4 +- systray/{ => assets}/sat.ico | Bin systray/{ => assets}/systray.gif | Bin systray/src/main.rs | 18 +- systray/src/systray.rs | 2 +- terminal/Cargo.toml | 4 +- terminal/README.md | 3 + terminal/{ => assets}/terminal.gif | Bin terminal/src/main.rs | 18 +- tinyskia/Cargo.toml | 4 +- tinyskia/README.md | 2 +- tinyskia/{ => assets}/ex.jpg | Bin tinyskia/{ => assets}/tinyskia.gif | Bin tinyskia/src/main.rs | 26 +- web-todo/Cargo.toml | 2 +- web-todo/README.md | 4 +- web-todo/{ => assets}/ex.jpg | Bin web-todo/{ => assets}/web-todo.gif | Bin web-todo/src/main.rs | 3 +- web-todo2/Cargo.toml | 2 +- web-todo2/README.md | 2 +- web-todo2/{ => assets}/ex.jpg | Bin web-todo2/{ => assets}/web-todo2.gif | Bin web-todo2/src/main.rs | 17 +- webview/Cargo.toml | 4 +- webview/README.md | 2 +- webview/{ => assets}/ex.jpg | Bin webview/{ => assets}/webview.gif | Bin webview/src/main.rs | 14 +- wgpu/Cargo.toml | 2 +- wgpu/README.md | 4 +- wgpu/{ => assets}/wgpu.gif | Bin xterm/Cargo.toml | 2 +- xterm/src/main.rs | 35 +- 195 files changed, 4128 insertions(+), 799 deletions(-) create mode 100644 7gui/Cargo.toml create mode 100644 7gui/README.md create mode 100644 7gui/src/components/cells.rs create mode 100644 7gui/src/components/circles.rs create mode 100644 7gui/src/components/counter.rs create mode 100644 7gui/src/components/crud.rs create mode 100644 7gui/src/components/flightbooker.rs create mode 100644 7gui/src/components/mod.rs create mode 100644 7gui/src/components/temperature.rs create mode 100644 7gui/src/components/timer.rs create mode 100644 7gui/src/main.rs rename csv/{ => assets}/csv.gif (100%) rename csv/{ => assets}/ex.jpg (100%) rename csv/{ => assets}/historical_data/GME.csv (100%) rename csv/{ => assets}/historical_data/dlpn.csv (100%) rename csv/{ => assets}/historical_data/oil.csv (100%) rename {egui-demo => egui}/Cargo.toml (100%) rename {egui-demo => egui}/README.md (71%) rename egui-demo/egui-demo.gif => egui/assets/egui.gif (100%) rename {egui-demo => egui/assets}/egui.jpg (100%) rename {egui-demo => egui}/src/main.rs (92%) rename femtovg/{ => assets}/ex.png (100%) rename femtovg/{ => assets}/femtovg.gif (100%) create mode 100644 flmusic/Cargo.toml create mode 100644 flmusic/README.md create mode 100644 flmusic/assets/base.png create mode 100644 flmusic/assets/base_dark.png create mode 100644 flmusic/src/components/mod.rs create mode 100644 flmusic/src/main.rs create mode 100644 flpicture/Cargo.toml create mode 100644 flpicture/README.md create mode 100644 flpicture/assets/base.png create mode 100644 flpicture/assets/base_dark.png create mode 100644 flpicture/src/components/mod.rs create mode 100644 flpicture/src/main.rs create mode 100644 flresters/Cargo.toml create mode 100644 flresters/LICENSE create mode 100644 flresters/README.md create mode 100644 flresters/assets/base_dark_linux.png create mode 100644 flresters/assets/full_dark_linux.png create mode 100644 flresters/src/main.rs create mode 100644 fltext/Cargo.toml create mode 100644 fltext/LICENSE create mode 100644 fltext/README.md create mode 100644 fltext/src/cbs.rs create mode 100644 fltext/src/dialogs.rs create mode 100644 fltext/src/fbr.rs create mode 100644 fltext/src/gui.rs create mode 100644 fltext/src/highlight/colors.rs create mode 100644 fltext/src/highlight/md.rs create mode 100644 fltext/src/highlight/mod.rs create mode 100644 fltext/src/highlight/rust.rs create mode 100644 fltext/src/highlight/toml.rs create mode 100644 fltext/src/main.rs create mode 100644 fltext/src/state.rs create mode 100644 fltext/src/utils.rs rename framebuffer/{ => assets}/ex.jpg (100%) rename framebuffer/{ => assets}/framebuffer.gif (100%) rename glium/{ => assets}/ex.jpg (100%) rename glium/{ => assets}/glium.gif (100%) rename glow/{ => assets}/ex.jpg (100%) rename glow/{ => assets}/glow.gif (100%) delete mode 100644 glut/.gitignore rename glut/{ => assets}/ex.png (100%) rename glut/{ => assets}/glut.gif (100%) rename glyphmap/{ => assets}/glyphmap.gif (100%) rename glyphmap/{ => assets}/image.jpg (100%) rename image/{ => assets}/fltk.png (100%) create mode 100644 inner/Cargo.toml create mode 100644 inner/README.md create mode 100644 inner/src/main.rs rename libvlc/{ => assets}/ex.jpg (100%) rename libvlc/{ => assets}/video.mp4 (100%) rename musicplayer/{ => assets}/Alarm.mp3 (100%) rename musicplayer/{ => assets}/musicplayer.gif (100%) rename musicplayer/{ => assets}/musicplayer.png (100%) rename opengl/{ => assets}/ex.jpg (100%) rename opengl/{ => assets}/opengl.gif (100%) rename pixels/{ => assets}/ex.jpg (100%) rename pixels/{ => assets}/pixels.gif (100%) rename plotters/{ => assets}/ex.jpg (100%) rename plotters/{ => assets}/plotters.gif (100%) rename raqote/{ => assets}/ex.jpg (100%) rename raqote/{ => assets}/raqote.gif (100%) rename rounded-svg/{ => assets}/rounded-svg.gif (100%) rename speedy2d/{ => assets}/ex.jpg (100%) rename speedy2d/{ => assets}/speedy2d.gif (100%) rename systray/{ => assets}/sat.ico (100%) rename systray/{ => assets}/systray.gif (100%) create mode 100644 terminal/README.md rename terminal/{ => assets}/terminal.gif (100%) rename tinyskia/{ => assets}/ex.jpg (100%) rename tinyskia/{ => assets}/tinyskia.gif (100%) rename web-todo/{ => assets}/ex.jpg (100%) rename web-todo/{ => assets}/web-todo.gif (100%) rename web-todo2/{ => assets}/ex.jpg (100%) rename web-todo2/{ => assets}/web-todo2.gif (100%) rename webview/{ => assets}/ex.jpg (100%) rename webview/{ => assets}/webview.gif (100%) rename wgpu/{ => assets}/wgpu.gif (100%) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f09401b..b1474ef 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -3,9 +3,11 @@ name: Build on: push: - branches: [ master ] + branches: + - master pull_request: - branches: [ master ] + branches: + - master schedule: - cron: '30 13 * * *' @@ -16,6 +18,7 @@ concurrency: jobs: build: runs-on: ${{ matrix.os }} + timeout-minutes: 120 strategy: matrix: os: @@ -25,14 +28,14 @@ jobs: steps: - name: Download deps + shell: bash run: | - if [ "$RUNNER_OS" == "Linux" ]; then + if [[ ${RUNNER_OS} == "Linux" ]]; then sudo apt-get update sudo apt-get install -y lib{pango1.0,x11,xext,xft,xinerama,mpv}-dev\ - lib{xcursor,xrender,xfixes,webkit2gtk-4.0,vlc,png,gl1-mesa}-dev\ + lib{xcursor,xrender,xfixes,webkit2gtk-4.1,vlc,png,gl1-mesa}-dev\ ninja-build libglu1-mesa-dev fi - shell: bash - uses: actions/checkout@v2 - uses: seanmiddleditch/gha-setup-ninja@master - name: build cfltk @@ -40,7 +43,7 @@ jobs: git clone https://github.com/MoAlyousef/cfltk pushd cfltk || return 1 git submodule update --init --recursive - case "${RUNNER_OS}" in + case ${RUNNER_OS} in Linux ) cmake -Bbin -GNinja -DOPTION_USE_SYSTEM_LIBPNG=ON -DOPTION_USE_SYSTEM_LIBJPEG=OFF -DOPTION_USE_SYSTEM_ZLIB=OFF -DCFLTK_LINK_IMAGES=ON -DOpenGL_GL_PREFERENCE=GLVND -DOPTION_USE_GL=ON -DCFLTK_USE_OPENGL=ON -DCFLTK_SINGLE_THREADED=OFF -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCFLTK_CARGO_BUILD=ON -DFLTK_BUILD_EXAMPLES=OFF -DFLTK_BUILD_TEST=OFF -DOPTION_LARGE_FILE=ON -DOPTION_USE_THREADS=ON -DOPTION_BUILD_HTML_DOCUMENTATION=OFF -DOPTION_BUILD_PDF_DOCUMENTATION=OFF -DCMAKE_INSTALL_PREFIX=bin -DCMAKE_BUILD_TYPE=Release -DOPTION_USE_PANGO=ON;; macOS ) cmake -Bbin -GNinja -DOPTION_USE_SYSTEM_LIBPNG=OFF -DOPTION_USE_SYSTEM_LIBJPEG=OFF -DOPTION_USE_SYSTEM_ZLIB=OFF -DCFLTK_LINK_IMAGES=ON -DOpenGL_GL_PREFERENCE=GLVND -DOPTION_USE_GL=ON -DCFLTK_USE_OPENGL=ON -DCFLTK_SINGLE_THREADED=OFF -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCFLTK_CARGO_BUILD=ON -DFLTK_BUILD_EXAMPLES=OFF -DFLTK_BUILD_TEST=OFF -DOPTION_LARGE_FILE=ON -DOPTION_USE_THREADS=ON -DOPTION_BUILD_HTML_DOCUMENTATION=OFF -DOPTION_BUILD_PDF_DOCUMENTATION=OFF -DCMAKE_INSTALL_PREFIX=bin -DCMAKE_BUILD_TYPE=Release;; * ) cmake -Bbin -GNinja -DOPTION_USE_SYSTEM_LIBPNG=OFF -DOPTION_USE_SYSTEM_LIBJPEG=OFF -DOPTION_USE_SYSTEM_ZLIB=OFF -DCFLTK_LINK_IMAGES=ON -DOpenGL_GL_PREFERENCE=GLVND -DOPTION_USE_GL=ON -DCFLTK_USE_OPENGL=ON -DCFLTK_SINGLE_THREADED=OFF -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCFLTK_CARGO_BUILD=ON -DFLTK_BUILD_EXAMPLES=OFF -DFLTK_BUILD_TEST=OFF -DOPTION_LARGE_FILE=ON -DOPTION_USE_THREADS=ON -DOPTION_BUILD_HTML_DOCUMENTATION=OFF -DOPTION_BUILD_PDF_DOCUMENTATION=OFF -DCMAKE_INSTALL_PREFIX=bin -DCMAKE_BUILD_TYPE=Release;; @@ -52,130 +55,202 @@ jobs: working-directory: cairo shell: bash run: | - if [ "$RUNNER_OS" == "Linux" ]; then - cargo build --verbose + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose fi + - name: Build 7gui + working-directory: 7gui + shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build cairo_shadow_button working-directory: cairo_shadow_button shell: bash run: | - if [ "$RUNNER_OS" == "Linux" ]; then - cargo build --verbose + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose fi - name: Build calendar working-directory: calendar shell: bash - run: cargo build --verbose + run: | + cargo build --quiet || cargo build --verbose - name: Build csv working-directory: csv - run: cargo build --verbose shell: bash + run: | + cargo build --quiet || cargo build --verbose + - name: Build egui + working-directory: egui + shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build femtovg working-directory: femtovg - run: cargo build --verbose shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build ffmpeg + working-directory: ffmpeg + shell: bash run: | - case "$RUNNER_OS" in - Linux | macOS) cargo build --verbose;; + case ${RUNNER_OS} in + Linux | macOS) cargo build --quiet || cargo build --verbose;; esac - working-directory: ffmpeg + - name: Build flcalculator + working-directory: flcalculator + shell: bash + run: | + cargo build --quiet || cargo build --verbose + - name: Build fldialect + working-directory: fldialect + shell: bash + run: | + cargo build --quiet || cargo build --verbose + - name: Build flmusic + working-directory: flmusic shell: bash + run: | + cargo build --quiet || cargo build --verbose + - name: Build flpicture + working-directory: flpicture + shell: bash + run: | + cargo build --quiet || cargo build --verbose + - name: Build flresters + working-directory: flresters + shell: bash + run: | + cargo build --quiet || cargo build --verbose + - name: Build fltext + working-directory: fltext + shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build framebuffer - run: cargo build --verbose working-directory: framebuffer shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build glium - run: cargo build --verbose working-directory: glium shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build glow - run: cargo build --verbose working-directory: glow shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build glut - run: cargo build --verbose working-directory: glut shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build image - run: cargo build --verbose working-directory: image shell: bash - - name: Build libvlc run: | - if [ "$RUNNER_OS" == "Linux" ]; then - cargo build --verbose + cargo build --quiet || cargo build --verbose + - name: Build inner + working-directory: inner + shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose fi + - name: Build libvlc working-directory: libvlc shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi - name: Build mpv - run: cargo build --verbose working-directory: mpv shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi - name: Build musicplayer - run: cargo build --verbose working-directory: musicplayer shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build opengl - run: cargo build --verbose working-directory: opengl shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build pixels - run: cargo build --verbose working-directory: pixels shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build plotters - run: cargo build --verbose working-directory: plotters shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build raqote - run: cargo build --verbose working-directory: raqote shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build rounded-svg - run: cargo build --verbose working-directory: rounded-svg shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build speedy2d - run: cargo build --verbose working-directory: speedy2d shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build systray - run: cargo build --verbose working-directory: systray shell: bash + run: | + if [[ ${RUNNER_OS} == "Windows" ]]; then + cargo build --quiet || cargo build --verbose + fi + - name: Build terminal + working-directory: terminal + shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build tinyskia - run: cargo build --verbose working-directory: tinyskia shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build web-todo - run: cargo build --verbose working-directory: web-todo shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build web-todo2 - run: cargo build --verbose working-directory: web-todo2 shell: bash + run: | + cargo build --quiet || cargo build --verbose - name: Build webview - run: cargo build --verbose working-directory: webview shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi - name: Build wgpu - run: cargo build --verbose working-directory: wgpu shell: bash - - name: Build xterm run: | - if [ "$RUNNER_OS" == "Linux" ]; then - cargo build --verbose - fi + cargo build --quiet || cargo build --verbose + - name: Build xterm working-directory: xterm shell: bash - - name: Build egui-demo - run: cargo build --verbose - working-directory: egui-demo - shell: bash - - name: Build terminal - run: cargo build --verbose - working-directory: terminal - shell: bash + run: | + if [[ ${RUNNER_OS} == "Linux" ]]; then + cargo build --quiet || cargo build --verbose + fi diff --git a/7gui/Cargo.toml b/7gui/Cargo.toml new file mode 100644 index 0000000..53a6b77 --- /dev/null +++ b/7gui/Cargo.toml @@ -0,0 +1,12 @@ +[package] +homepage = "https://github.com/fltk-rs" +authors = ["Artem V. Ageev"] +name = "fl7gui" +version = "0.0.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fltk = { version = "^1.4", features = ["use-ninja"] } +chrono = "^0.4" diff --git a/7gui/README.md b/7gui/README.md new file mode 100644 index 0000000..8ba17f5 --- /dev/null +++ b/7gui/README.md @@ -0,0 +1,4 @@ +![FLCalculator](assets/base_light_linux.png "FLCalculator") +![FLCalculator](assets/base_dark_linux.png "FLCalculator") +![FLCalculator](assets/base_light_macos.png "FLCalculator") +![FLCalculator](assets/base_dark_macos.png "FLCalculator") diff --git a/7gui/src/components/cells.rs b/7gui/src/components/cells.rs new file mode 100644 index 0000000..d2e2359 --- /dev/null +++ b/7gui/src/components/cells.rs @@ -0,0 +1,636 @@ +use fltk::{app::*, draw::*, enums::*, input::*, prelude::*, table::*, window::*}; +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::character::complete::{alpha1, digit1, multispace0, not_line_ending}; +use nom::combinator::{all_consuming, cut, eof, map, verify}; +use nom::multi::separated_list1; +use nom::number::complete::float; +use nom::sequence::{delimited, pair, preceded, separated_pair, terminated, tuple}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::char; +use std::cmp::{max, min}; +use std::collections::HashMap; +use std::collections::HashSet; +use std::convert::TryFrom; +use std::convert::TryInto; +use std::fmt; +use std::rc::Rc; +use std::str::FromStr; + +struct Row(usize); + +impl fmt::Display for Row { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0 + 1) + } +} + +struct Col(usize); + +impl fmt::Display for Col { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + char::from_u32(u32::from('A') + u32::try_from(self.0).unwrap()).unwrap() + ) + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] +struct Coord { + row: usize, + col: usize, +} +impl Coord { + fn new(row: usize, col: usize) -> Self { + Self { row, col } + } + fn parse(input: &str) -> nom::IResult<&str, Self> { + let alpha = map(alpha1, |s| { + usize::from_str_radix(s, 36).unwrap() - usize::from_str_radix("A", 36).unwrap() + }); + let num = verify(map(digit1, |s| usize::from_str(s).unwrap()), |num| *num > 0); + map(tuple((alpha, num)), |(col, row)| Self::new(row - 1, col))(input) + } +} + +#[derive(Debug, PartialEq)] +enum Formula { + Empty, + Coord(Coord), + Range(Coord, Coord), + Number(f32), + Label(String), + Application(String, Vec), + Invalid, +} +impl Formula { + fn parse(input: &str) -> nom::IResult<&str, Formula> { + alt(( + preceded( + tag("="), + cut(all_consuming(preceded(multispace0, Self::parse_expr))), + ), + map(eof, |_| Formula::Empty), + map(all_consuming(float), Formula::Number), + map(all_consuming(not_line_ending), |s: &str| { + Formula::Label(s.to_string()) + }), + ))(input) + } + fn parse_expr(input: &str) -> nom::IResult<&str, Formula> { + alt(( + map( + separated_pair(Coord::parse, tag(":"), Coord::parse), + |(start, end)| Formula::Range(start, end), + ), + map(Coord::parse, Formula::Coord), + map(float, Formula::Number), + map( + pair( + alpha1, + terminated( + delimited( + preceded(multispace0, tag("(")), + separated_list1( + preceded(multispace0, tag(",")), + preceded(multispace0, Self::parse_expr), + ), + preceded(multispace0, tag(")")), + ), + multispace0, + ), + ), + |(name, args)| Formula::Application(name.to_string(), args), + ), + ))(input) + } + fn references(&self) -> Vec { + match self { + Formula::Coord(coord) => vec![*coord], + Formula::Range(start, end) => { + let mut refs = vec![]; + for col in min(start.col, end.col)..=max(start.col, end.col) { + for row in min(start.row, end.row)..=max(start.row, end.row) { + refs.push(Coord::new(row, col)); + } + } + refs + } + Formula::Application(_func, args) => { + args.iter().flat_map(|arg| arg.references()).collect() + } + _ => vec![], + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct CellError; + +#[derive(Debug)] +struct Cell { + text: String, + formula: Formula, + value: Result, + subscribers: HashSet, +} +impl Default for Cell { + fn default() -> Self { + Self { + text: String::new(), + formula: Formula::Empty, + value: Ok(f32::NAN), + subscribers: HashSet::new(), + } + } +} + +struct Spreadsheet { + cells: HashMap, + default_cell: Cell, +} +impl Spreadsheet { + fn new() -> Self { + Self { + cells: HashMap::new(), + default_cell: Cell::default(), + } + } + fn cell(&self, coord: Coord) -> &Cell { + self.cells.get(&coord).unwrap_or(&self.default_cell) + } + fn set_text(&mut self, coord: Coord, text: &str) { + let new_formula = Formula::parse(text) + .map(|(_, output)| output) + .unwrap_or(Formula::Invalid); + + // Unsubscribe cells referenced by the old formula + let old_formula = &self.cell(coord).formula; + for referenced_coord in old_formula.references() { + let referenced_cell = self.cells.entry(referenced_coord).or_default(); + referenced_cell.subscribers.remove(&coord); + } + + // Subscribe cells referenced by the new formula + for referenced_coord in new_formula.references() { + let referenced_cell = self.cells.entry(referenced_coord).or_default(); + referenced_cell.subscribers.insert(coord); + } + + let cell = self.cells.entry(coord).or_default(); + cell.text = text.to_string(); + cell.formula = new_formula; + + self.update_value(coord); + } + fn update_value(&mut self, coord: Coord) { + let cell = self.cell(coord); + let old_value = cell.value; + let new_value = self.evaluate(&cell.formula); + if old_value != new_value { + let cell = self.cells.entry(coord).or_default(); + cell.value = new_value; + + // Update cells that are subscribed to this cell + let subscribers = cell.subscribers.iter().copied().collect::>(); + for subscribed_coord in subscribers { + self.update_value(subscribed_coord); + } + } + } + fn evaluate(&self, formula: &Formula) -> Result { + match formula { + Formula::Empty => Ok(f32::NAN), + Formula::Coord(coord) => Ok(self.cell(*coord).value?), + Formula::Range(_start, _end) => Err(CellError), + Formula::Number(number) => Ok(*number), + Formula::Label(_) => Ok(f32::NAN), + Formula::Application(func, args) => { + let mut values: Vec = Vec::new(); + for arg in args { + for arg_val in self.evaluate_list(arg)? { + values.push(arg_val); + } + } + let is_binary_op = values.len() == 2; + match func.to_ascii_lowercase().as_ref() { + "add" if is_binary_op => Ok(values[0] + values[1]), + "sub" if is_binary_op => Ok(values[0] - values[1]), + "div" if is_binary_op => Ok(values[0] / values[1]), + "mul" if is_binary_op => Ok(values[0] * values[1]), + "mod" if is_binary_op => Ok(values[0] % values[1]), + "sum" => Ok(values.iter().sum()), + "prod" => Ok(values.iter().product()), + _ => Err(CellError), + } + } + Formula::Invalid => Err(CellError), + } + } + fn evaluate_list(&self, formula: &Formula) -> Result, CellError> { + match formula { + Formula::Range(_start, _end) => formula + .references() + .iter() + .map(|coord| self.cell(*coord).value) + .collect(), + formula => Ok(vec![self.evaluate(formula)?]), + } + } +} + +struct SpreadsheetWidgetInner { + table: Table, + input: Input, + sheet: Spreadsheet, + edit_cell: std::option::Option, +} + +impl SpreadsheetWidgetInner { + fn start_editing(&mut self, row: i32, col: i32) { + self.edit_cell = Some(Coord::new(row.try_into().unwrap(), col.try_into().unwrap())); + } + fn finish_editing(&mut self) { + if let Some(edit_cell) = self.edit_cell { + let text = self.input.value(); + self.sheet.set_text(edit_cell, &text); + self.edit_cell = None; + self.input.hide(); + // Prevent mouse cursor from remaining hidden after pressing Enter. + self.input.window().unwrap().set_cursor(Cursor::Default); + } + } + fn draw_cell( + &mut self, + table_context: TableContext, + row: i32, + col: i32, + x: i32, + y: i32, + width: i32, + height: i32, + ) { + push_clip(x, y, width, height); + fltk::draw::set_font(Font::Helvetica, 16); + match table_context { + TableContext::RowHeader => { + let color = self.table.row_header_color(); + draw_box(FrameType::GtkThinUpBox, x, y, width, height, color); + set_draw_color(Color::Black); + draw_text2( + &Row(row.try_into().unwrap()).to_string(), + x, + y, + width, + height, + Align::Bottom | Align::Center, + ); + } + TableContext::ColHeader => { + let color = self.table.col_header_color(); + draw_box(FrameType::GtkThinUpBox, x, y, width, height, color); + set_draw_color(Color::Black); + draw_text2( + &Col(col.try_into().unwrap()).to_string(), + x, + y, + width, + height, + Align::Bottom | Align::Center, + ); + } + TableContext::Cell => { + let coord = Coord::new(row.try_into().unwrap(), col.try_into().unwrap()); + if self.edit_cell == Some(coord) { + self.input.resize(x, y, width, height); + self.input.show(); + self.input.set_value(&self.sheet.cell(coord).text); + self.input.take_focus().expect("input refused focus"); + self.input.redraw(); + } else { + let color = if self.table.is_selected(row, col) { + Color::from_u32(0x0099ff) + } else { + Color::BackGround2 + }; + let cell = self.sheet.cell(coord); + let text = match (cell.value, &cell.formula) { + (Err(_), _) => Cow::from("ERROR"), + (Ok(_), Formula::Label(_)) => Cow::from(&cell.text), + (Ok(_), Formula::Empty) => Cow::from(""), + (Ok(value), _) => Cow::from(value.to_string()), + }; + draw_box(FrameType::GtkThinUpBox, x, y, width, height, color); + set_draw_color(Color::Black); + draw_text2(&text, x, y, width, height, Align::Bottom | Align::Left); + } + } + _ => {} + } + pop_clip(); + } +} + +struct SpreadsheetWidget { + _inner: Rc>, +} + +impl SpreadsheetWidget { + fn new(x: i32, y: i32, width: i32, height: i32, sheet: Spreadsheet) -> Self { + let mut table = Table::new(x, y, width, height, ""); + table.end(); // Table implicitly calls begin. + table.set_row_header(true); + table.set_col_header(true); + table.set_rows(100); + table.set_cols(26); + + let mut cell_input = Input::default().with_size(75, 25).with_pos(0, 0); + cell_input.set_trigger(CallbackTrigger::EnterKeyAlways); + cell_input.hide(); + + let inner = Rc::new(RefCell::new(SpreadsheetWidgetInner { + table, + input: cell_input, + sheet, + edit_cell: None, + })); + + let inner_clone = inner.clone(); + inner.borrow_mut().input.set_callback(move |_| { + inner_clone.borrow_mut().finish_editing(); + }); + + let inner_clone = inner.clone(); + inner.borrow_mut().table.draw_cell( + move |_, table_context, row, col, x, y, width, height| { + inner_clone + .borrow_mut() + .draw_cell(table_context, row, col, x, y, width, height); + }, + ); + + let inner_clone = inner.clone(); + inner.borrow_mut().table.handle(move |widget, event| { + if event == Event::Push && widget.callback_context() == TableContext::Cell { + inner_clone.borrow_mut().finish_editing(); + if event_clicks() { + inner_clone + .borrow_mut() + .start_editing(widget.callback_row(), widget.callback_col()); + } + widget.redraw(); + true + } else { + false + } + }); + + Self { _inner: inner } + } +} + +fn main() { + let app = App::default().with_scheme(Scheme::Gtk); + let mut wind = Window::default().with_label("Cells"); + let sheet = Spreadsheet::new(); + let _sheet_widget = SpreadsheetWidget::new(0, 0, 400, 200, sheet); + wind.set_size(400, 200); + wind.end(); + wind.show(); + app.run().unwrap(); +} + +#[cfg(test)] +mod tests { + use super::{CellError, Coord, Formula, Spreadsheet}; + + macro_rules! spreadsheet( + { $($key:ident => $value:expr),+ } => { + { + let mut s = Spreadsheet::new(); + $(s.set_text(Coord::parse(stringify!($key)).unwrap().1, &$value.to_string());)+ + s + } + }; + ); + + macro_rules! coord( + ( $c:ident ) => { Coord::parse(stringify!($c)).unwrap().1 } + ); + + #[test] + fn coord_parsing_works() { + assert!(Coord::parse("A0").is_err()); + assert_eq!(Coord::parse("a1"), Ok(("", Coord::new(0, 0)))); + assert_eq!(Coord::parse("A1"), Ok(("", Coord::new(0, 0)))); + assert_eq!(Coord::parse("B2"), Ok(("", Coord::new(1, 1)))); + assert_eq!(Coord::parse("Z99"), Ok(("", Coord::new(98, 25)))); + } + + #[test] + fn formula_references_works() { + assert_eq!(Formula::Number(1.0).references(), []); + assert_eq!(Formula::Coord(coord!(A1)).references(), [coord!(A1)]); + assert_eq!( + Formula::Range(coord!(A1), coord!(B2)).references(), + [coord!(A1), coord!(A2), coord!(B1), coord!(B2)] + ); + assert_eq!( + Formula::Range(coord!(B2), coord!(A1)).references(), + [coord!(A1), coord!(A2), coord!(B1), coord!(B2)] + ); + assert_eq!( + Formula::Application( + "sum".to_string(), + vec![ + Formula::Coord(coord!(A1)), + Formula::Range(coord!(B1), coord!(C1)) + ] + ) + .references(), + [coord!(A1), coord!(B1), coord!(C1)] + ); + } + + #[test] + fn formula_parsing_works() { + assert_eq!(Formula::parse("1.0"), Ok(("", Formula::Number(1.0)))); + assert_eq!(Formula::parse("1.1"), Ok(("", Formula::Number(1.1)))); + assert_eq!(Formula::parse("10"), Ok(("", Formula::Number(10.0)))); + assert_eq!( + Formula::parse("1.0foo"), + Ok(("", Formula::Label("1.0foo".to_string()))) + ); + assert_eq!( + Formula::parse("foo"), + Ok(("", Formula::Label("foo".to_string()))) + ); + assert_eq!(Formula::parse(""), Ok(("", Formula::Empty))); + assert_eq!(Formula::parse("=1.0"), Ok(("", Formula::Number(1.0)))); + assert!(Formula::parse("=1.0foo").is_err()); + assert_eq!(Formula::parse("=A1"), Ok(("", Formula::Coord(coord!(A1))))); + assert_eq!(Formula::parse("=A2"), Ok(("", Formula::Coord(coord!(A2))))); + assert_eq!( + Formula::parse("=A2:C4"), + Ok(("", Formula::Range(coord!(A2), coord!(C4)))) + ); + assert_eq!( + Formula::parse("=sum(A1,sub(A2,1.0))"), + Ok(( + "", + Formula::Application( + "sum".to_string(), + vec![ + Formula::Coord(coord!(A1)), + Formula::Application( + "sub".to_string(), + vec![Formula::Coord(coord!(A2)), Formula::Number(1.0),], + ) + ], + ) + )) + ); + assert_eq!( + Formula::parse("= sum ( A1 , A2 ) "), + Ok(( + "", + Formula::Application( + "sum".to_string(), + vec![Formula::Coord(coord!(A1)), Formula::Coord(coord!(A2))] + ) + )) + ); + } + + #[test] + fn spreadsheet_works() { + let mut sheet = Spreadsheet::new(); + + assert!(sheet.cell(coord!(A1)).value.unwrap().is_nan()); + assert_eq!(sheet.cell(coord!(A1)).text, ""); + + sheet.set_text(coord!(A1), ""); + assert!(sheet.cell(coord!(A1)).value.unwrap().is_nan()); + assert_eq!(sheet.cell(coord!(A1)).text, ""); + + sheet.set_text(coord!(A1), "foobar"); + assert!(sheet.cell(coord!(A1)).value.unwrap().is_nan()); + assert_eq!(sheet.cell(coord!(A1)).text, "foobar"); + + sheet.set_text(coord!(A1), "1.1"); + assert_eq!(sheet.cell(coord!(A1)).value, Ok(1.1)); + assert_eq!(sheet.cell(coord!(A1)).text, "1.1"); + + sheet.set_text(coord!(A1), "=1.1"); + assert_eq!(sheet.cell(coord!(A1)).value, Ok(1.1)); + assert_eq!(sheet.cell(coord!(A1)).text, "=1.1"); + + sheet.set_text(coord!(A1), "2.0"); + sheet.set_text(coord!(B2), "=A1"); + assert_eq!(sheet.cell(coord!(B2)).value, Ok(2.0)); + + sheet.set_text(coord!(B2), "=A2"); + assert!(sheet.cell(coord!(B2)).value.unwrap().is_nan()); + + sheet.set_text(coord!(A1), "=FOOBAR()"); + assert_eq!(sheet.cell(coord!(A1)).value, Err(CellError)); + + sheet.set_text(coord!(B2), "=A1"); + assert_eq!(sheet.cell(coord!(B2)).value, Err(CellError)); + } + + #[test] + fn function_with_wrong_arguments_returns_error() { + let sheet = spreadsheet! {A1 => "=ADD(A2)"}; + assert_eq!(sheet.cell(coord!(A1)).value, Err(CellError)); + } + + #[test] + fn function_add_works() { + let sheet = spreadsheet! {A1 => 3, B1 => 5, C1 => "=ADD(A1,B1)"}; + assert_eq!(sheet.cell(coord!(C1)).value, Ok(8.0)); + } + + #[test] + fn function_sub_works() { + let sheet = spreadsheet! {A1 => 3, B1 => 5, C1 => "=SUB(A1,B1)"}; + assert_eq!(sheet.cell(coord!(C1)).value, Ok(-2.0)); + } + + #[test] + fn function_div_works() { + let sheet = spreadsheet! {A1 => 10, B1 => 2, C1 => "=DIV(A1,B1)"}; + assert_eq!(sheet.cell(coord!(C1)).value, Ok(5.0)); + } + + #[test] + fn function_mul_works() { + let sheet = spreadsheet! {A1 => 10, B1 => 2, C1 => "=MUL(A1,B1)"}; + assert_eq!(sheet.cell(coord!(C1)).value, Ok(20.0)); + } + + #[test] + fn function_mod_works() { + let sheet = spreadsheet! {A1 => 22, B1 => 10, C1 => "=MOD(A1,B1)"}; + assert_eq!(sheet.cell(coord!(C1)).value, Ok(2.0)); + } + + #[test] + fn function_sum_works() { + let sheet = spreadsheet! { + A1 => 1, B1 => 2, C1 => 3, + A2 => "=SUM(A1,B1,C1)" + }; + assert_eq!(sheet.cell(coord!(A2)).value, Ok(6.0)); + } + + #[test] + fn function_prod_works() { + let sheet = spreadsheet! { + A1 => 3, B1 => 5, C1 => 7, + A2 => "=PROD(A1,B1,C1)" + }; + assert_eq!(sheet.cell(coord!(A2)).value, Ok(105.0)); + } + + #[test] + fn application_with_range_argument_works() { + let sheet = spreadsheet! { + A1 => 1, B1 => 2, C1 => 3, + A2 => "=SUM(A1:C1)" + }; + assert_eq!(sheet.cell(coord!(A2)).value, Ok(6.0)); + } + + #[test] + fn application_with_application_argument_works() { + let sheet = spreadsheet! { + A1 => 1, B1 => 2, C1 => 3, + A2 => "=PROD(A1,SUM(B1,C1))" + }; + assert_eq!(sheet.cell(coord!(A2)).value, Ok(5.0)); + } + + #[test] + fn nested_application_works() { + let sheet = spreadsheet! {A1 => 3, B1 => 5, C1 => "=PROD(A1,SUM(A1,B1))"}; + assert_eq!(sheet.cell(coord!(C1)).value, Ok(24.0)); + } + + #[test] + fn changes_propagate() { + let mut sheet = spreadsheet! {A1 => 1, B1 => "=A1"}; + sheet.set_text(coord!(A1), "2"); + assert_eq!(sheet.cell(coord!(B1)).value, Ok(2.0)); + } + + #[test] + fn old_cell_references_are_removed() { + let mut sheet = spreadsheet! {A1 => 1, B1 => "=A1"}; + sheet.set_text(coord!(B1), "2"); + assert!(sheet.cells.get(&coord!(A1)).unwrap().subscribers.is_empty()) + } +} diff --git a/7gui/src/components/circles.rs b/7gui/src/components/circles.rs new file mode 100644 index 0000000..a93478a --- /dev/null +++ b/7gui/src/components/circles.rs @@ -0,0 +1,246 @@ +use { + crate::Message, + fltk::{ + app::*, button::*, draw::*, enums::*, group::*, menu::*, prelude::*, valuator::*, widget::*, + }, + std::{ + cell::RefCell, + collections::HashMap, + ops::{Deref, DerefMut}, + rc::Rc, + }, +}; + +const WIDGET_PADDING: i32 = 10; + +const CANVAS_WIDTH: i32 = 350; +const CANVAS_HEIGHT: i32 = 250; + +const MOUSE_LEFT: i32 = 1; +const MOUSE_RIGHT: i32 = 3; + +type Coord = (i32, i32); +type Radius = i32; + +fn distance(a: Coord, b: Coord) -> i32 { + f64::sqrt(f64::from(i32::pow(a.0 - b.0, 2) + i32::pow(a.1 - b.1, 2))) as i32 +} + +pub struct Model { + sender: Sender, + states: Vec>, + current_state_index: usize, +} +impl Model { + fn build(sender: Sender) -> Self { + Self { + sender, + states: vec![HashMap::new()], + current_state_index: 0, + } + } + pub fn save(&mut self) { + for _ in self.current_state_index + 1..self.states.len() { + self.states.pop(); + } + self.states + .push(self.states[self.current_state_index].clone()); + self.current_state_index += 1; + } + pub fn undo(&mut self) { + assert!(self.can_undo()); + self.current_state_index -= 1; + self.sender.send(Message::ModelChanged); + } + fn can_undo(&self) -> bool { + self.current_state_index > 0 + } + pub fn redo(&mut self) { + assert!(self.can_redo()); + self.current_state_index += 1; + self.sender.send(Message::ModelChanged); + } + fn can_redo(&self) -> bool { + self.current_state_index + 1 < self.states.len() + } + pub fn set(&mut self, coord: Coord, radius: Radius) { + self.states[self.current_state_index].insert(coord, radius); + self.sender.send(Message::ModelChanged); + } + fn circles(&self) -> &HashMap { + &self.states[self.current_state_index] + } +} + +struct Canvas { + widget: Widget, + circles: Rc>>, + selected: Rc>>, +} + +impl Canvas { + fn new(x: i32, y: i32, width: i32, height: i32, sender: Sender) -> Self { + let mut canvas = Canvas { + widget: Widget::new(x, y, width, height, ""), + circles: Rc::default(), + selected: Rc::default(), + }; + canvas.widget.set_trigger(CallbackTrigger::Release); + + let circles = canvas.circles.clone(); + let selected = canvas.selected.clone(); + canvas.widget.handle(move |widget, event| { + let event_pos = (event_x() - widget.x(), event_y() - widget.y()); + match event { + Event::Enter => true, + Event::Move => { + let new_selection = circles + .borrow() + .iter() + .map(|(pos, radius)| (*pos, *radius)) + .filter(|(pos, radius)| distance(*pos, event_pos) <= *radius) + .min_by_key(|(pos, _radius)| distance(*pos, event_pos)); + if new_selection != *selected.borrow() { + widget.redraw(); + selected.replace(new_selection); + } + true + } + Event::Released if event_button() == MOUSE_LEFT => { + if selected.borrow().is_none() { + sender.send(Message::Add(event_pos)); + } + true + } + Event::Released if event_button() == MOUSE_RIGHT => { + let selected = *selected.borrow(); // Limit borrow lifetime. + if selected.is_some() { + let menu = MenuItem::new(&["Adjust diameter..."]); + if menu.popup(event_x(), event_y()).is_some() { + sender.send(Message::AdjustOpened); + } + } + true + } + _ => false, + } + }); + + let circles = canvas.circles.clone(); + let selected = canvas.selected.clone(); + canvas.widget.draw(move |wid| { + push_clip(wid.x(), wid.y(), wid.width(), wid.height()); + draw_rect_fill(wid.x(), wid.y(), wid.width(), wid.height(), Color::White); + for (pos, radius) in &*circles.borrow() { + let draw_x = wid.x() + pos.0 - radius; + let draw_y = wid.y() + pos.1 - radius; + let diameter = radius * 2; + if matches!(*selected.borrow(), Some((selected_pos, _)) if selected_pos == *pos) { + set_draw_color(Color::from_rgb(200, 200, 200)); + draw_pie(draw_x, draw_y, diameter, diameter, 0.0, 360.0); + } + set_draw_color(Color::Black); + draw_arc(draw_x, draw_y, diameter, diameter, 0.0, 360.0); + } + set_draw_color(Color::Black); + draw_rect(wid.x(), wid.y(), wid.width(), wid.height()); + pop_clip(); + }); + + canvas + } + fn set_circles(&mut self, circles: &HashMap) { + self.circles.replace(circles.clone()); + self.redraw(); + } + fn selected(&self) -> std::option::Option<(Coord, Radius)> { + *self.selected.borrow() + } +} + +impl Deref for Canvas { + type Target = Widget; + + fn deref(&self) -> &Self::Target { + &self.widget + } +} + +impl DerefMut for Canvas { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.widget + } +} + +pub struct Circles { + pub model: Model, + canvas: Canvas, + diameter: HorNiceSlider, + undo: Button, + redo: Button, +} + +impl Circles { + pub fn build(sender: Sender) -> Self { + let mut flex = Flex::default_fill().with_label(" Circles ").column(); + let row = Flex::default_fill(); + let mut undo = Button::default().with_label("Undo"); + undo.emit(sender, Message::Undo); + let mut redo = Button::default().with_label("Redo"); + redo.emit(sender, Message::Redo); + row.end(); + flex.fixed(&row, 35); + let canvas = Canvas::new( + WIDGET_PADDING, + undo.y() + undo.height() + WIDGET_PADDING, + CANVAS_WIDTH, + CANVAS_HEIGHT, + sender, + ); + let row = Flex::default_fill(); + let mut diameter = HorNiceSlider::default().with_align(Align::Left); + diameter.set_align(Align::Top | Align::Left); + diameter.set_minimum(1.0); + diameter.set_maximum(50.0); + diameter.emit(sender, Message::RadiusChanged); + row.end(); + flex.fixed(&row, 25); + flex.end(); + flex.set_margin(10); + flex.set_pad(10); + sender.send(Message::ModelChanged); + Self { + model: Model::build(sender), + canvas, + diameter, + undo, + redo, + } + } + pub fn opened(&mut self) { + self.model.save(); + let (pos, radius) = self.canvas.selected().unwrap(); + self.diameter + .set_label(&format!("Adjust diameter of circle at {:?}.", pos)); + self.diameter.set_value(f64::from(radius)); + } + pub fn radius_changed(&mut self) { + self.model.set( + self.canvas.selected().unwrap().0, + self.diameter.value() as Radius, + ) + } + pub fn model_changed(&mut self) { + set_activated(&mut self.undo, self.model.can_undo()); + set_activated(&mut self.redo, self.model.can_redo()); + self.canvas.set_circles(self.model.circles()); + } +} + +fn set_activated(widget: &mut T, is_activated: bool) { + if is_activated { + widget.activate(); + } else { + widget.deactivate(); + } +} diff --git a/7gui/src/components/counter.rs b/7gui/src/components/counter.rs new file mode 100644 index 0000000..6d0400c --- /dev/null +++ b/7gui/src/components/counter.rs @@ -0,0 +1,44 @@ +use crate::Message; +use fltk::{app::Sender, button::*, group::*, output::*, prelude::*}; + +pub struct Counter { + model: u8, + out: Output, + pub inc: Button, + pub dec: Button, +} + +impl Counter { + pub fn build(sender: Sender) -> Self { + let mut flex = Flex::default_fill().with_label(" Counter ").column(); + let mut out = Output::default(); + let row = Flex::default_fill(); + let mut dec = Button::default().with_label("-"); + let mut inc = Button::default().with_label("+"); + row.end(); + out.set_value("0"); + inc.emit(sender, Message::CounterInc); + dec.emit(sender, Message::CounterDec); + flex.end(); + flex.set_margin(10); + flex.set_pad(10); + Self { + model: 0, + out, + inc, + dec, + } + } + pub fn inc(&mut self) { + if self.model < 255 { + self.model += 1; + self.out.set_value(&self.model.to_string()); + } + } + pub fn dec(&mut self) { + if self.model > 0 { + self.model -= 1; + self.out.set_value(&self.model.to_string()); + } + } +} diff --git a/7gui/src/components/crud.rs b/7gui/src/components/crud.rs new file mode 100644 index 0000000..e286518 --- /dev/null +++ b/7gui/src/components/crud.rs @@ -0,0 +1,106 @@ +use crate::Message; +use fltk::{ + app::Sender, browser::*, button::*, enums::*, frame::*, group::*, input::*, prelude::*, +}; + +pub struct Crud { + model: Vec, + pub browser: HoldBrowser, + pub filter: Input, + firstname: Input, + lastname: Input, + pub create: Button, + pub update: Button, + pub delete: Button, +} + +impl Crud { + pub fn build(sender: Sender) -> Self { + let mut flex = Flex::default_fill().with_label(" CRUD ").column(); + let mut row = Flex::default_fill(); + let col = Flex::default_fill().column(); + let mut browser = HoldBrowser::default(); + col.end(); + let col = Flex::default_fill().column(); + Frame::default(); + col.end(); + row.fixed(&col, 150); + let col = Flex::default_fill().column(); + let firstname = Input::default().with_label("Name:"); + let lastname = Input::default().with_label("Surname:"); + let mut filter = Input::default().with_label("Filter prefix:"); + col.end(); + row.end(); + let col = Flex::default_fill(); + let mut create = Button::default().with_label("Create"); + let mut update = Button::default().with_label("Update"); + let mut delete = Button::default().with_label("Delete"); + col.end(); + flex.fixed(&col, 35); + flex.end(); + flex.set_margin(10); + flex.set_pad(10); + filter.set_trigger(CallbackTrigger::Changed); + update.deactivate(); + delete.deactivate(); + browser.emit(sender, Message::CrudSelect); + filter.emit(sender, Message::CrudRead); + create.emit(sender, Message::CrudCreate); + update.emit(sender, Message::CrudUpdate); + delete.emit(sender, Message::CrudDelete); + let mut component = Self { + model: Vec::new(), + browser, + filter, + firstname, + lastname, + create, + update, + delete, + }; + component.filter(); + component + } + pub fn select(&mut self) { + if self.browser.value() == 0 { + self.update.deactivate(); + self.delete.deactivate(); + } else { + self.update.activate(); + self.delete.activate(); + } + } + pub fn filter(&mut self) { + let prefix = self.filter.value().to_lowercase(); + self.browser.clear(); + for item in &self.model { + if item.to_lowercase().starts_with(&prefix) { + self.browser.add(item); + } + } + self.browser.sort(); + self.browser.select(1); + self.select(); + } + pub fn create(&mut self) { + self.model.push(format!( + "{}, {}", + self.lastname.value(), + self.firstname.value() + )); + self.filter(); + } + pub fn update(&mut self) { + let selected_name = self.browser.text(self.browser.value()).unwrap(); + let index = self.model.iter().position(|s| s == &selected_name).unwrap(); + self.model[index] = format!("{}, {}", self.lastname.value(), self.firstname.value()); + self.filter(); + } + pub fn delete(&mut self) { + let selected_name = self.browser.text(self.browser.value()).unwrap(); + let index = self.model.iter().position(|s| s == &selected_name).unwrap(); + self.model.remove(index); + self.filter(); + self.select(); + } +} diff --git a/7gui/src/components/flightbooker.rs b/7gui/src/components/flightbooker.rs new file mode 100644 index 0000000..94b7841 --- /dev/null +++ b/7gui/src/components/flightbooker.rs @@ -0,0 +1,96 @@ +use crate::Message; +use chrono::{offset::Local, NaiveDate}; +use fltk::{app::Sender, button::*, dialog::*, enums::*, group::*, input::*, menu::*, prelude::*}; + +pub struct FlightBooker { + pub choice: Choice, + pub start: Input, + pub back: Input, + pub book: Button, +} + +impl FlightBooker { + pub fn build(sender: Sender) -> Self { + let mut flex = Flex::default_fill() + .with_label(" FlightBooker ") + .column(); + let row = Flex::default_fill(); + let mut choice = Choice::default(); + let mut start = Input::default(); + row.end(); + let row = Flex::default_fill(); + let mut back = Input::default(); + let mut book = Button::default().with_label("Book"); + row.end(); + let current: &str = &Local::now() + .naive_local() + .date() + .format("%Y-%m-%d") + .to_string(); + flex.end(); + flex.set_margin(10); + flex.set_pad(10); + choice.add_choice("one-way flight|return flight"); + choice.set_value(0); + start.set_trigger(CallbackTrigger::Changed); + start.set_value(current); + back.deactivate(); + back.set_trigger(CallbackTrigger::Changed); + back.set_value(current); + choice.emit(sender, Message::Update); + start.emit(sender, Message::Update); + back.emit(sender, Message::Update); + book.emit(sender, Message::Book); + Self { + choice, + start, + back, + book, + } + } + pub fn update(&mut self) { + if self.choice.value() == 0 { + self.back.deactivate(); + if get_date(&mut self.start).is_ok() { + self.book.activate(); + } else { + self.book.deactivate(); + } + } else { + self.back.activate(); + let start_date = get_date(&mut self.start); + let return_date = get_date(&mut self.back); + if start_date.is_ok() + && return_date.is_ok() + && start_date.unwrap() <= return_date.unwrap() + { + self.book.activate(); + } else { + self.book.deactivate(); + } + } + } + pub fn book(&mut self) { + alert_default(&if self.choice.value() == 0 { + format!( + "You have booked a one-way flight for {}.", + self.start.value() + ) + } else { + format!( + "You have booked a return flight from {} to {}", + self.start.value(), + self.back.value() + ) + }); + } +} +fn get_date(input: &mut Input) -> Result { + let date = NaiveDate::parse_from_str(&input.value(), "%Y-%m-%d"); + input.set_color(match date { + Ok(_) => Color::BackGround2, + Err(_) => Color::Red, + }); + input.redraw(); + date +} diff --git a/7gui/src/components/mod.rs b/7gui/src/components/mod.rs new file mode 100644 index 0000000..bf68c66 --- /dev/null +++ b/7gui/src/components/mod.rs @@ -0,0 +1,11 @@ +mod circles; +mod counter; +mod crud; +mod flightbooker; +mod temperature; +mod timer; + +pub use { + circles::Circles, counter::Counter, crud::Crud, flightbooker::FlightBooker, + temperature::Temperature, timer::Timer, +}; diff --git a/7gui/src/components/temperature.rs b/7gui/src/components/temperature.rs new file mode 100644 index 0000000..626d1c0 --- /dev/null +++ b/7gui/src/components/temperature.rs @@ -0,0 +1,45 @@ +use crate::Message; +use fltk::{app::Sender, enums::*, frame::*, group::*, input::*, prelude::*}; + +pub struct Temperature { + pub celsius: IntInput, + pub fahrenheit: IntInput, +} + +impl Temperature { + pub fn build(sender: Sender) -> Self { + let mut flex = Flex::default_fill().with_label(" Temperature "); + flex.fixed(&Frame::default(), 100); + let col = Flex::default_fill().column(); + let mut celsius = IntInput::default().with_label("Celsius = "); + let mut fahrenheit = IntInput::default().with_label("Fahrenheit = "); + col.end(); + flex.end(); + flex.set_margin(10); + flex.set_pad(10); + celsius.set_trigger(CallbackTrigger::Changed); + fahrenheit.set_trigger(CallbackTrigger::Changed); + celsius.emit(sender, Message::Celsius); + fahrenheit.emit(sender, Message::Fahrenheit); + Self { + celsius, + fahrenheit, + } + } + pub fn celsius(&mut self) { + if let Ok(celsius) = self.celsius.value().parse::() { + let value = f64::from(celsius) * (9.0 / 5.0) + 32.0; + self.fahrenheit.set_value(&format!("{}", value.round())) + } else { + self.fahrenheit.set_value(""); + } + } + pub fn fahrenheit(&mut self) { + if let Ok(fahrenheit) = self.fahrenheit.value().parse::() { + let value = (f64::from(fahrenheit) - 32.0) * (5.0 / 9.0); + self.celsius.set_value(&format!("{}", value.round())) + } else { + self.celsius.set_value(""); + } + } +} diff --git a/7gui/src/components/timer.rs b/7gui/src/components/timer.rs new file mode 100644 index 0000000..d429a51 --- /dev/null +++ b/7gui/src/components/timer.rs @@ -0,0 +1,43 @@ +use crate::Message; +use fltk::{app::Sender, button::*, enums::*, group::*, misc::*, prelude::*, valuator::*}; +use std::{thread, time::Duration}; + +pub struct Timer { + pub progress: Progress, + pub slider: HorSlider, + pub button: Button, +} + +impl Timer { + pub fn build(sender: Sender) -> Self { + let mut flex = Flex::default_fill().with_label(" Timer ").column(); + let mut component = Timer { + progress: Progress::default().with_label("Elapsed Time: 0.0s"), + slider: HorSlider::default(), + button: Button::default().with_label("Reset"), + }; + flex.end(); + flex.set_margin(10); + flex.set_pad(10); + const DURATION_DEFAULT: f64 = 15.0; + const DURATION_MAXIMUM: f64 = 30.0; + component.progress.set_selection_color(Color::Blue); + component.progress.set_maximum(DURATION_DEFAULT); + component.slider.set_value(DURATION_DEFAULT); + component.slider.set_maximum(DURATION_MAXIMUM); + component.slider.emit(sender, Message::ChangeDuration); + component.button.emit(sender, Message::Reset); + thread::spawn(move || loop { + thread::sleep(Duration::from_millis(100)); + sender.send(Message::Tick); + }); + component + } + pub fn tick(&mut self) { + if self.slider.value() - self.progress.value() >= 0.01 { + self.progress.set_value(self.progress.value() + 0.1); + self.progress + .set_label(&format!("Elapsed Time: {:.1}s", self.progress.value())); + } + } +} diff --git a/7gui/src/main.rs b/7gui/src/main.rs new file mode 100644 index 0000000..87ba923 --- /dev/null +++ b/7gui/src/main.rs @@ -0,0 +1,97 @@ +#![forbid(unsafe_code)] + +mod components; + +use { + components::{Circles, Counter, Crud, FlightBooker, Temperature, Timer}, + fltk::{ + app, + enums::{Event, Font}, + group::{Flex, Tabs}, + prelude::{GroupExt, ValuatorExt, WidgetBase, WidgetExt, WindowExt}, + window::Window, + }, +}; + +#[derive(Clone, Copy)] +pub enum Message { + CounterInc, + CounterDec, + Celsius, + Fahrenheit, + Update, + Book, + Reset, + ChangeDuration, + Tick, + CrudSelect, + CrudCreate, + CrudRead, + CrudUpdate, + CrudDelete, + Undo, + Redo, + Add((i32, i32)), + AdjustOpened, + RadiusChanged, + ModelChanged, + Quit(bool), +} + +pub fn main() { + let mut window = Window::default() + .with_size(960, 540) + .with_label("Fl7GUI") + .center_screen(); + let mut flex = Flex::default_fill(); + let mut tabs = Tabs::default_fill(); + let (sender, receiver) = app::channel::(); + let mut counter = Counter::build(sender); + let mut temperature = Temperature::build(sender); + let mut flightbooker = FlightBooker::build(sender); + let mut timer = Timer::build(sender); + let mut crud = Crud::build(sender); + let mut circles = Circles::build(sender); + tabs.end(); + tabs.auto_layout(); + flex.end(); + flex.set_margin(10); + window.end(); + window.make_resizable(true); + window.show(); + window.emit(sender, Message::Quit(false)); + app::set_font(Font::Courier); + while app::App::default().with_scheme(app::AppScheme::Plastic).wait() { + match receiver.recv() { + Some(Message::CounterInc) => counter.inc(), + Some(Message::CounterDec) => counter.dec(), + Some(Message::Celsius) => temperature.celsius(), + Some(Message::Fahrenheit) => temperature.fahrenheit(), + Some(Message::Update) => flightbooker.update(), + Some(Message::Book) => flightbooker.book(), + Some(Message::Reset) => timer.progress.set_value(0.0), + Some(Message::ChangeDuration) => timer.progress.set_maximum(timer.slider.value()), + Some(Message::Tick) => timer.tick(), + Some(Message::CrudSelect) => crud.select(), + Some(Message::CrudCreate) => crud.create(), + Some(Message::CrudRead) => crud.filter(), + Some(Message::CrudUpdate) => crud.update(), + Some(Message::CrudDelete) => crud.delete(), + Some(Message::Undo) => circles.model.undo(), + Some(Message::Redo) => circles.model.redo(), + Some(Message::AdjustOpened) => circles.opened(), + Some(Message::RadiusChanged) => circles.radius_changed(), + Some(Message::ModelChanged) => circles.model_changed(), + Some(Message::Add(pos)) => { + circles.model.save(); + circles.model.set(pos, 20); + } + Some(Message::Quit(force)) => { + if force || app::event() == Event::Close { + app::quit(); + } + } + None => {} + } + } +} diff --git a/README.md b/README.md index 51e4c2f..9af1b57 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- +

fltk-rs demos

[![Documentation](https://docs.rs/fltk/badge.svg)](https://docs.rs/fltk) @@ -64,135 +64,34 @@ This repository is licensed under the MIT license. You can get more information --- The current demos include: -* [🏜️ cairo:](#cairo) Use cairo for custom drawing inside fltk widgets -* [🗓️  calendar:](#calendar) Uses the chrono crate to create an fltk calendar dialog. -- 🌐 web-todo: Creating an async web todo app using fltk, reqwest, serde and tokio. -- 🌐 web-todo2: Creating an async web todo app using fltk, surf, serde and async-std. -- 📺 libvlc: Creating a media player using fltk and the vlc crate. -- 🎶 musicplayer: Creating a music player using custom widgets and the soloud crate. -- 🎨 opengl: Raw OpenGL drawing in an fltk GlWindow. -- 🖌️ glut: Use the gl crate (An OpenGL function pointer loader) to do OpenGL drawing. -- 🖊️ wgpu: Use wgpu-rs for gpu accelerated drawing. -- 🎞️ pixels: Use the pixels crate to draw a wgpu accelerated framebuffer. -- ✒️ framebuffer: Using fltk for framebuffer drawing. -- 🌌 plotters: Use plotters for live plotting (drawing animations) with fltk. -- 🌈 raqote: Use raqote for custom drawing (paint example). -- 🖼️ tinyskia: Use tiny-skia for custom drawing. -- 🖥️ systray: Use nwg to create an fltk app with systray functionalities on Windows -- ✨ glow: Use the glow crate to do OpengGL drawing. -- 🎇 glium: Use the glium crate for OpenGL drawing. -- 🏞️ image: Uses rust-embed and the image crates to load images into fltk. -- 🌚 speedy2d: Uses speedy2D crate to do 2D drawings of a circle and an RGB image in a GlWindow. -- 🪐 femtovg: Uses femtovg for 2D drawing in a GlWindow. -- 📽️ ffmpeg: Uses ffmpeg for software video rendering. -- 💻 webview: Embeds a webview inside an fltk app. -- 🖍️ csv: Uses serde and csv to perform custom drawing of data. -- 🔘 rounded-svg: Use the svg crate along with fltk to create images with rounded borders. -- 🔦 libmpv: use libmpv to play a video inside an fltk GlWindow. --  🧥 mpv: mpv: Use mpv (the command line app) to play a video inside an fltk window. -- 📲 xterm: embed an xterm window inside an fltk window. -- 🎛️ egui-demo: Use fltk as a backend for egui -- 🎞️ gst: Use libgstreamer to play a video inside an fltk window -- 📍 glyphmap: Maps glyphs (specifically font icons) to their unicode codepoint. -- 📟 terminal: A minimal terminal emulator. -
+* [🏜️ cairo:](/cairo) Use cairo for custom drawing inside fltk widgets +* [🗓️  calendar:](/calendar) Uses the chrono crate to create an fltk calendar dialog. +* [🖍️  csv:](/csv) Uses serde and csv to perform custom drawing of data. +* [🎛️ egui:](/egui) Use fltk as a backend for egui +* [🪐 femtovg:](/femtovg) Uses femtovg for 2D drawing in a GlWindow. +* [📽️ ffmpeg:](/ffmpeg) Uses ffmpeg for software video rendering. +* [✒️ framebuffer:](/framebuffer) Using fltk for framebuffer drawing. +* [🎇 glium:](/glium) Use the glium crate for OpenGL drawing. +* [✨  glow:](/glow) Use the glow crate to do OpengGL drawing. +* [🖌️  glut:](/glut) Use the gl crate (An OpenGL function pointer loader) to do OpenGL drawing. +* [📍  glyphmap:](/glyphmap) Maps glyphs (specifically font icons) to their unicode codepoint. +* [🎞️ gst:](/gst) Use libgstreamer to play a video inside an fltk window +* [🏞️ image:](/image) Uses rust-embed and the image crates to load images into fltk. +* [🔦  libmpv:](/libmpv) use libmpv to play a video inside an fltk GlWindow. +* [📺  libvlc:](/libvlc) Creating a media player using fltk and the vlc crate. +* [🎶 musicplayer:](/musicplayer) Creating a music player using custom widgets and the soloud crate. +* [🎨  opengl:](/opengl) Raw OpenGL drawing in an fltk GlWindow. +* [🎞️ pixels:](/pixels) Use the pixels crate to draw a wgpu accelerated framebuffer. +* [🌌 plotters:](/plotters) Use plotters for live plotting (drawing animations) with fltk. +* [🌈  raqote:](/raqote) Use raqote for custom drawing (paint example). +* [🖼️  tinyskia:](/tinyskia) Use tiny-skia for custom drawing. +* [🌚 speedy2d:](/speedy2d) Uses speedy2D crate to do 2D drawings of a circle and an RGB image in a GlWindow. +* [🖥️ systray:](/systray) Use nwg to create an fltk app with systray functionalities on Windows +* [💻 webview:](/webview) Embeds a webview inside an fltk app. +* [🔘 rounded-svg:](/rounded-svg) Use the svg crate along with fltk to create images with rounded borders. +* [📟 terminal:](/terminal) A minimal terminal emulator. +* [🌐  web-todo:](/web-todo) Creating an async web todo app using fltk, reqwest, serde and tokio. +* [🌐  web-todo2:](/web-todo2) Creating an async web todo app using fltk, surf, serde and async-std. +* [🖊️ wgpu:](/wgpu) Use wgpu-rs for gpu accelerated drawing. --- - -
- -

musicplayer

- -![musicplayer](musicplayer/musicplayer.gif) - -

web-todo

- -![web-todo](web-todo/web-todo.gif) - -

opengl

- -![opengl](opengl/opengl.gif) - -

glut

- -![glut](glut/glut.gif) - -

pixels

- -![pixels](pixels/pixels.gif) - -

plotters

- -![plotters](plotters/plotters.gif) - -

raqote

- -![raqote](raqote/raqote.gif) - -

tinyskia

- -![tinyskia](tinyskia/tinyskia.gif) - -

glow

- -![glow](glow/glow.gif) - -

glium

- -![glium](glium/glium.gif) - -

calendar

- -![calendar](calendar/assets/calendar.gif) - -

speedy2d

- -![speedy2d](speedy2d/speedy2d.gif) - -

femtovg

- -![femtovg](femtovg/femtovg.gif) - -

webview

- -![webview](webview/webview.gif) - -

csv

- -![csv](csv/csv.gif) - -

egui-demo

- -![egui-demo](egui-demo/egui-demo.gif) - -

glyphmap

- -![glyphmap](glyphmap/glyphmap.gif) - -

terminal

- -![terminal](terminal/terminal.gif) - -

rounded-svg

- -![rounded-svg](rounded-svg/rounded-svg.gif) - -

systray

- -![systray](systray/systray.gif) - -

wgpu

- -![wgpu](wgpu/wgpu.gif) - -

cairo

- -![cairo](cairo/assets/ex.jpg) - -
- -

cairo shadow button

- -![cairo](cairo_shadow_button/assets/ex1.jpg) - - diff --git a/cairo/Cargo.toml b/cairo/Cargo.toml index 3e0e958..7f9c290 100644 --- a/cairo/Cargo.toml +++ b/cairo/Cargo.toml @@ -1,11 +1,11 @@ [package] +authors = ["Mohammed Alyousef "] name = "cairo_demo" version = "0.1.0" -authors = ["Mohammed Alyousef "] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = { version = "^1.4", features = ["cairoext"] } +fltk = { version = "^1.4", features = ["use-ninja", "cairoext"] } cairo-rs = "^0.19" diff --git a/cairo/src/main.rs b/cairo/src/main.rs index 0ee7f4c..13f6502 100644 --- a/cairo/src/main.rs +++ b/cairo/src/main.rs @@ -1,4 +1,5 @@ #![allow(dead_code)] +#[cfg(target_os = "linux")] use cairo::Context; use fltk::{draw::Rect, enums::*, prelude::*, *}; use std::{cell::RefCell, rc::Rc}; @@ -77,7 +78,7 @@ impl CairoWidget { fltk::widget_extends!(CairoWidget, frame::Frame, frm); fn main() { - let app = app::App::default().with_scheme(app::AppScheme::Gtk); + let app = app::App::default().with_scheme(app::AppScheme::Base); let mut win = window::Window::default() .with_label("Demo: Cairo") .with_size(400, 300) diff --git a/cairo_shadow_button/Cargo.toml b/cairo_shadow_button/Cargo.toml index 3c086b5..bef10e2 100644 --- a/cairo_shadow_button/Cargo.toml +++ b/cairo_shadow_button/Cargo.toml @@ -1,9 +1,10 @@ [package] +authors = ["Mohammed Alyousef "] name = "cairo_button" version = "0.1.0" edition = "2021" [dependencies] -fltk = "1.4" +fltk = { version = "^1.4", features = ["use-ninja"] } cairo-rs = "0.18" -cairo-blur = "0.1.0" \ No newline at end of file +cairo-blur = "^0.1" diff --git a/cairo_shadow_button/src/main.rs b/cairo_shadow_button/src/main.rs index 68864d0..6fb4e7b 100644 --- a/cairo_shadow_button/src/main.rs +++ b/cairo_shadow_button/src/main.rs @@ -1,6 +1,9 @@ #![forbid(unsafe_code)] -use cairo::{Context, Format, ImageSurface}; -use fltk::{enums::*, prelude::*, *}; +#[cfg(target_os = "linux")] +use { + cairo::{Context, Format, ImageSurface}, + fltk::{enums::*, prelude::*, *}, +}; #[derive(Clone)] struct CairoButton { @@ -72,7 +75,7 @@ impl CairoButton { fltk::widget_extends!(CairoButton, button::Button, btn); fn main() { - let app = app::App::default().with_scheme(app::AppScheme::Gtk); + let app = app::App::default().with_scheme(app::AppScheme::Base); let mut win = window::Window::default() .with_label("Demo: Cairo") .with_size(600, 600) diff --git a/calendar/Cargo.toml b/calendar/Cargo.toml index 7290f50..236b445 100644 --- a/calendar/Cargo.toml +++ b/calendar/Cargo.toml @@ -1,11 +1,11 @@ [package] +authors = ["MoAlyousef "] name = "calendar" version = "0.1.1" -authors = ["MoAlyousef "] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" -chrono = "0.4" +fltk = { version = "^1.4", features = ["use-ninja"] } +chrono = "^0.4" diff --git a/calendar/README.md b/calendar/README.md index b9c415a..321197c 100644 --- a/calendar/README.md +++ b/calendar/README.md @@ -2,4 +2,4 @@ An example fltk-rs calendar dialog using the `chrono` crate spawned from the command-line. Double click to choose the date. -![alt_test](assets/ex.jpg) +![alt_test](assets/calendar.gif) diff --git a/calendar/src/calendar.rs b/calendar/src/calendar.rs index 6e69183..4d99dfd 100644 --- a/calendar/src/calendar.rs +++ b/calendar/src/calendar.rs @@ -1,7 +1,4 @@ -use chrono::DateTime; -use chrono::Datelike; -use chrono::Local; -use chrono::NaiveDate; +use chrono::{DateTime, Datelike, Local, NaiveDate}; use fltk::{app, draw, enums::*, menu, prelude::*, table, window}; use std::{cell::RefCell, rc::Rc}; @@ -39,8 +36,8 @@ impl Calendar { table.set_col_width_all(table.width() / 7); table.set_row_height_all(table.height() / 4 - table.col_header_height()); table.end(); - wind.make_modal(true); wind.end(); + wind.make_modal(true); wind.show(); let curr = Rc::from(RefCell::from(curr + 1)); @@ -93,14 +90,14 @@ impl Calendar { _ => (), }); - let curr_rc = curr.clone(); + let curr_rc = curr; // redraw table when the month changes month_choice.set_callback(move |c| { *curr_rc.borrow_mut() = c.value() + 1; c.parent().unwrap().redraw(); }); - let curr_year_rc = curr_year.clone(); + let curr_year_rc = curr_year; // redraw table when the year changes year_choice.set_callback(move |c| { *curr_year_rc.borrow_mut() = c.value() + 1900; diff --git a/calendar/src/main.rs b/calendar/src/main.rs index 1f32242..b0b6a43 100644 --- a/calendar/src/main.rs +++ b/calendar/src/main.rs @@ -1,24 +1,27 @@ +#![forbid(unsafe_code)] mod calendar; use chrono::prelude::*; use fltk::{prelude::*, *}; fn main() { - let app = app::App::default().with_scheme(app::AppScheme::Gtk); + let app = app::App::default().with_scheme(app::AppScheme::Plastic); let mut win = window::Window::default() .with_label("Demo: Calendar") .with_size(400, 300) .center_screen(); - button::Button::new(160, 200, 80, 40, "Click").set_callback(move |_| { - let cal = calendar::Calendar::default(); // or calendar::Calendar::new(200, 100); - let date = cal.get_date(); - println!("{:?}", date); - if let Some(date) = date { - println!("{:?}", date.year()); - println!("{:?}", date.month()); - println!("{:?}", date.day()); - } - }); + button::Button::default() + .with_label("Click") + .with_size(80, 40) + .center_of_parent() + .set_callback(move |_| { + let cal = calendar::Calendar::default(); // or calendar::Calendar::new(200, 100); + if let Some(date) = cal.get_date() { + println!("{:?}", date.year()); + println!("{:?}", date.month()); + println!("{:?}", date.day()); + } + }); win.end(); win.make_resizable(true); win.show(); diff --git a/csv/Cargo.toml b/csv/Cargo.toml index 1a1c4e3..a344307 100644 --- a/csv/Cargo.toml +++ b/csv/Cargo.toml @@ -1,14 +1,14 @@ [package] -name = "csv" -version = "0.1.0" authors = ["MoAlyousef "] +name = "csv_demo" +version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" -csv = "1.1.6" -serde = { version = "1", features = ["derive"] } -lazy_static = "1.4" -image = { version = "0.24.5", default-features = false, features = ["jpeg"] } \ No newline at end of file +fltk = { version = "^1.4", features = ["use-ninja"] } +csv = "^1.3" +serde = { version = "^1.0", features = ["derive"] } +lazy_static = "^1.4" +image = { version = "^0.25", default-features = false, features = ["jpeg"] } diff --git a/csv/README.md b/csv/README.md index 58b0114..b1a14b7 100644 --- a/csv/README.md +++ b/csv/README.md @@ -2,4 +2,4 @@ Custom drawing of CSV data. Uses CSV and Serde. -![alt_test](ex.jpg) \ No newline at end of file +![alt_test](assets/csv.gif) diff --git a/csv/csv.gif b/csv/assets/csv.gif similarity index 100% rename from csv/csv.gif rename to csv/assets/csv.gif diff --git a/csv/ex.jpg b/csv/assets/ex.jpg similarity index 100% rename from csv/ex.jpg rename to csv/assets/ex.jpg diff --git a/csv/historical_data/GME.csv b/csv/assets/historical_data/GME.csv similarity index 100% rename from csv/historical_data/GME.csv rename to csv/assets/historical_data/GME.csv diff --git a/csv/historical_data/dlpn.csv b/csv/assets/historical_data/dlpn.csv similarity index 100% rename from csv/historical_data/dlpn.csv rename to csv/assets/historical_data/dlpn.csv diff --git a/csv/historical_data/oil.csv b/csv/assets/historical_data/oil.csv similarity index 100% rename from csv/historical_data/oil.csv rename to csv/assets/historical_data/oil.csv diff --git a/csv/src/main.rs b/csv/src/main.rs index 987c893..a3ee580 100644 --- a/csv/src/main.rs +++ b/csv/src/main.rs @@ -1,9 +1,6 @@ +#![forbid(unsafe_code)] use ::image::{ImageBuffer, RgbImage}; -use fltk::{ - enums::*, - prelude::*, - *, -}; +use fltk::{enums::*, prelude::*, *}; use serde::Deserialize; use std::sync::Mutex; @@ -13,7 +10,7 @@ extern crate lazy_static; #[derive(Debug, Deserialize)] pub struct Price { #[serde(rename = "Date")] - date: String, + _date: String, #[serde(rename = "Open")] open: f64, #[serde(rename = "High")] @@ -23,7 +20,7 @@ pub struct Price { #[serde(rename = "Close")] close: f64, #[serde(rename = "Volume")] - volume: usize, + _volume: usize, } lazy_static! { @@ -31,36 +28,46 @@ lazy_static! { } fn main() { - let files = std::fs::read_dir("historical_data").unwrap(); - let app = app::App::default().with_scheme(app::Scheme::Gtk); - app::background(79, 79, 79); - app::background2(41, 41, 41); - app::foreground(255, 255, 255); + let app = app::App::default().with_scheme(app::AppScheme::Gtk); let mut wind = window::Window::default().with_size(800, 600); - wind.make_resizable(true); let mut browser = browser::Browser::new(5, 10, 100, 520, ""); let mut frame = frame::Frame::default() .with_size(680, 520) .right_of(&browser, 10); - let mut btn = button::Button::default() + button::Button::default() .with_label("Save image") .with_size(100, 30) .below_of(&frame, 15) - .center_x(&wind); + .center_x(&wind) + .set_callback({ + let frame = frame.clone(); + move |_| { + let sur = surface::ImageSurface::new(frame.w(), frame.h(), false); + surface::ImageSurface::push_current(&sur); + draw::set_draw_color(enums::Color::White); + draw::draw_rectf(0, 0, frame.w(), frame.h()); + sur.draw(&frame, 0, 0); + let img = sur.image().unwrap(); + surface::ImageSurface::pop_current(); + let mut imgbuf: RgbImage = ImageBuffer::new(frame.w() as _, frame.h() as _); // this is from the image crate + imgbuf.copy_from_slice(&img.to_rgb_data()); + imgbuf.save("image.jpg").unwrap(); + } + }); + wind.end(); wind.make_resizable(true); wind.show(); browser.set_type(browser::BrowserType::Hold); - for file in files { + for file in std::fs::read_dir("assets/historical_data").unwrap() { let entry = file.unwrap().file_name().into_string().unwrap(); if entry.ends_with(".csv") { - browser.add(&entry.strip_suffix(".csv").unwrap()); + browser.add(entry.strip_suffix(".csv").unwrap()); } } frame.set_frame(FrameType::DownBox); frame.set_color(Color::Black); - frame.draw(|f| { let data = PRICES.lock().unwrap(); let mut highest = data @@ -69,7 +76,7 @@ fn main() { .collect::>() .iter() .cloned() - .fold(0. / 0., f64::max); + .fold(f64::NAN, f64::max); highest += (highest.to_string().len() * 10) as f64 / 3.; let factor = f.h() as f64 / highest; if data.len() != 0 { @@ -95,25 +102,9 @@ fn main() { } }); - btn.set_callback({ - let frame = frame.clone(); - move |_| { - let sur = surface::ImageSurface::new(frame.w(), frame.h(), false); - surface::ImageSurface::push_current(&sur); - draw::set_draw_color(enums::Color::White); - draw::draw_rectf(0, 0, frame.w(), frame.h()); - sur.draw(&frame, 0, 0); - let img = sur.image().unwrap(); - surface::ImageSurface::pop_current(); - let mut imgbuf: RgbImage = ImageBuffer::new(frame.w() as _, frame.h() as _); // this is from the image crate - imgbuf.copy_from_slice(&img.to_rgb_data()); - imgbuf.save("image.jpg").unwrap(); - } - }); - browser.set_callback(move |t| { if let Some(file) = t.selected_text() { - let file = format!("historical_data/{}.csv", file); + let file = format!("assets/historical_data/{}.csv", file); let mut rdr = csv::Reader::from_reader(std::fs::File::open(file).unwrap()); let mut data = PRICES.lock().unwrap(); data.clear(); @@ -125,5 +116,8 @@ fn main() { } }); + app::background(79, 79, 79); + app::background2(41, 41, 41); + app::foreground(255, 255, 255); app.run().unwrap(); } diff --git a/egui-demo/Cargo.toml b/egui/Cargo.toml similarity index 100% rename from egui-demo/Cargo.toml rename to egui/Cargo.toml diff --git a/egui-demo/README.md b/egui/README.md similarity index 71% rename from egui-demo/README.md rename to egui/README.md index 32cfbe0..a9582a2 100644 --- a/egui-demo/README.md +++ b/egui/README.md @@ -2,4 +2,4 @@ Use fltk as a backend for egui using the fltk-egui crate. -![alt_test](egui.jpg) \ No newline at end of file +![alt_test](assets/egui.gif) diff --git a/egui-demo/egui-demo.gif b/egui/assets/egui.gif similarity index 100% rename from egui-demo/egui-demo.gif rename to egui/assets/egui.gif diff --git a/egui-demo/egui.jpg b/egui/assets/egui.jpg similarity index 100% rename from egui-demo/egui.jpg rename to egui/assets/egui.jpg diff --git a/egui-demo/src/main.rs b/egui/src/main.rs similarity index 92% rename from egui-demo/src/main.rs rename to egui/src/main.rs index 74a3f89..3a632a8 100644 --- a/egui-demo/src/main.rs +++ b/egui/src/main.rs @@ -1,26 +1,16 @@ use egui_backend::{ egui, - fltk::{ - enums::*, - prelude::*, - *, - }, + fltk::{enums::*, prelude::*, *}, gl, DpiScaling, }; use fltk_egui as egui_backend; -use std::rc::Rc; -use std::{ - cell::RefCell, - time::Instant -}; +use std::{cell::RefCell, rc::Rc, time::Instant}; const SCREEN_WIDTH: u32 = 800; const SCREEN_HEIGHT: u32 = 600; fn main() { - let a = app::App::default().with_scheme(app::Scheme::Gtk); - app::get_system_colors(); - app::set_font_size(20); + let app = app::App::default().with_scheme(app::Scheme::Gtk); let mut main_win = window::Window::new(100, 100, SCREEN_WIDTH as _, SCREEN_HEIGHT as _, None); let mut glut_win = window::GlWindow::new(5, 5, main_win.w() - 200, main_win.h() - 10, None); glut_win.set_mode(Mode::Opengl3); @@ -29,7 +19,6 @@ fn main() { .column() .with_size(185, 590) .right_of(&glut_win, 5); - col.set_frame(FrameType::DownBox); let mut frm = frame::Frame::default(); frm.set_color(Color::Red.inactive()); frm.set_frame(FrameType::FlatBox); @@ -38,8 +27,9 @@ fn main() { slider.set_slider_size(0.20); slider.set_color(Color::Blue.inactive()); slider.set_selection_color(Color::Red); - col.set_size(&mut slider, 20); col.end(); + col.fixed(&slider, 20); + col.set_frame(FrameType::DownBox); main_win.end(); main_win.make_resizable(true); main_win.show(); @@ -77,7 +67,9 @@ fn main() { let mut age = 0; let mut quit = false; - while a.wait() { + app::get_system_colors(); + app::set_font_size(20); + while app.wait() { let mut state = state_rc.borrow_mut(); let mut painter = painter_rc.borrow_mut(); state.input.time = Some(start_time.elapsed().as_secs_f64()); @@ -118,7 +110,7 @@ fn main() { let paint_jobs = egui_ctx.tessellate(paint_cmds); //Draw egui texture - painter.paint_jobs(None, paint_jobs, &egui_ctx.texture()); + painter.paint_jobs(None, paint_jobs, &egui_ctx.font_image()); glut_win.swap_buffers(); glut_win.flush(); diff --git a/femtovg/Cargo.toml b/femtovg/Cargo.toml index 834af3e..a134dc7 100644 --- a/femtovg/Cargo.toml +++ b/femtovg/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "femtovg" -version = "0.1.0" authors = ["Mohammed Alyousef "] +name = "femtovg_demo" +version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = { version = "1", features = ["enable-glwindow"] } -femtovg = "0.1.1" +fltk = { version = "^1.4", features = ["use-ninja", "enable-glwindow"] } +femtovg = "^0.9" diff --git a/femtovg/README.md b/femtovg/README.md index a1377a3..dcb4770 100644 --- a/femtovg/README.md +++ b/femtovg/README.md @@ -2,4 +2,4 @@ A demo showing a gradient created with femtovg. -![alt_test](ex.png) \ No newline at end of file +![alt_test](assets/femtovg.gif) diff --git a/femtovg/ex.png b/femtovg/assets/ex.png similarity index 100% rename from femtovg/ex.png rename to femtovg/assets/ex.png diff --git a/femtovg/femtovg.gif b/femtovg/assets/femtovg.gif similarity index 100% rename from femtovg/femtovg.gif rename to femtovg/assets/femtovg.gif diff --git a/femtovg/src/main.rs b/femtovg/src/main.rs index 1c19ac4..528c34e 100644 --- a/femtovg/src/main.rs +++ b/femtovg/src/main.rs @@ -1,35 +1,26 @@ -use femtovg::{ - renderer:: - OpenGl, - Canvas, - Color, - Paint, - Path -}; -use fltk::{ - app, - enums, - prelude::{ - GroupExt, - WidgetBase, - WidgetExt, - WindowExt +use { + femtovg::{renderer::OpenGl, Canvas, Color, Paint, Path}, + fltk::{ + app, enums, + prelude::{GroupExt, WidgetBase, WidgetExt, WindowExt}, + window::GlWindow, }, - window::GlWindow, }; fn main() { - let app = app::App::default(); + let app = app::App::default().with_scheme(app::Scheme::Base); let mut win = GlWindow::default() - .with_size(640, 480) - .with_label("femtovg example"); + .with_label("femtovg example") + .with_size(640, 480); + win.end(); win.set_mode(enums::Mode::Opengl3); win.make_resizable(true); - win.end(); win.show(); win.make_current(); - let renderer = - OpenGl::new(|s| win.get_proc_address(s) as *const _).expect("Cannot create renderer"); + let renderer = unsafe { + OpenGl::new_from_function(|s| win.get_proc_address(s) as *const _) + .expect("Cannot create renderer") + }; let mut canvas = Canvas::new(renderer).expect("Cannot create canvas"); win.draw(move |w| { @@ -44,8 +35,8 @@ fn main() { let mut p = Path::new(); p.rect(0.0, 0.0, w.width() as _, w.height() as _); canvas.fill_path( - &mut p, - Paint::linear_gradient( + &p, + &Paint::linear_gradient( 0.0, 0.0, w.width() as _, diff --git a/ffmpeg/Cargo.toml b/ffmpeg/Cargo.toml index 3ac1607..e6f8efe 100644 --- a/ffmpeg/Cargo.toml +++ b/ffmpeg/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" +fltk = { version = "^1.4", features = ["use-ninja"] } signal-hook = "0.3" lazy_static = "1.4" diff --git a/ffmpeg/src/main.rs b/ffmpeg/src/main.rs index 08aef1d..66699c9 100644 --- a/ffmpeg/src/main.rs +++ b/ffmpeg/src/main.rs @@ -1,17 +1,15 @@ -use fltk::{ - app, - button, - enums::{ - Color, - FrameType +#![forbid(unsafe_code)] +use { + fltk::{ + app, button, + enums::{Color, FrameType}, + frame, image, + prelude::*, + window, }, - frame, - image, - prelude::*, - window, + signal_hook::{consts::signal::SIGINT, iterator::Signals}, + std::{cell, env, error, fs, process, rc, thread}, }; -use signal_hook::{consts::signal::SIGINT, iterator::Signals}; -use std::{cell, env, error, fs, process, rc, thread}; lazy_static::lazy_static! { pub static ref VIDEO_TEMP_DIR: String = env::temp_dir().join("video_mp4").to_string_lossy().to_string(); @@ -29,7 +27,7 @@ fn main() -> Result<(), Box> { fs::create_dir(&*VIDEO_TEMP_DIR).ok(); process::Command::new("ffmpeg") - .args(&[ + .args([ "-i", "../libvlc/video.mp4", &format!("{}/%d.bmp", &*VIDEO_TEMP_DIR), @@ -38,7 +36,7 @@ fn main() -> Result<(), Box> { .status() .unwrap(); - let mut signals = Signals::new(&[SIGINT])?; + let mut signals = Signals::new([SIGINT])?; thread::spawn(move || { for _sig in signals.forever() { fs::remove_dir_all(&*VIDEO_TEMP_DIR).unwrap(); @@ -48,7 +46,6 @@ fn main() -> Result<(), Box> { let _a = MyApp {}; let app = app::App::default(); let mut win = window::Window::default().with_size(600, 400); - win.make_resizable(true); let mut frame = frame::Frame::default() .with_size(400, 300) .center_of_parent(); @@ -56,6 +53,7 @@ fn main() -> Result<(), Box> { frame.set_color(Color::Black); let mut but = button::Button::new(260, 355, 80, 40, "@+6>"); win.end(); + win.make_resizable(true); win.show(); let i = rc::Rc::from(cell::RefCell::from(0)); diff --git a/flcalculator/Cargo.toml b/flcalculator/Cargo.toml index 4fb9124..7ab9722 100644 --- a/flcalculator/Cargo.toml +++ b/flcalculator/Cargo.toml @@ -1,6 +1,6 @@ [package] homepage = "https://github.com/fltk-rs" -authors = "Artem V. Ageev" +authors = ["Artem V. Ageev"] name = "flcalculator" version = "0.0.1" edition = "2021" @@ -8,4 +8,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = { version = "^1.4" } +fltk = { version = "^1.4", features = ["use-ninja"] } diff --git a/flcalculator/src/main.rs b/flcalculator/src/main.rs index 79ae43d..83b0f03 100644 --- a/flcalculator/src/main.rs +++ b/flcalculator/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + use { fltk::{ app, @@ -16,7 +18,7 @@ use { }; fn main() { - let file = env::var("HOME").unwrap() + CFG; + let file = env::var("HOME").unwrap() + "/.config/flcalculator"; let mut theme: u8 = match Path::new(&file).exists() { true => fs::read(&file).unwrap()[0], false => 0, @@ -61,7 +63,7 @@ fn main() { } menu.set_frame(FrameType::FlatBox); let mut window = Window::default() - .with_label("FlCalculator") + .with_label("flCalculator") .with_size(360, 640) .center_screen(); let mut vbox = Flex::default_fill().column(); @@ -144,7 +146,7 @@ fn main() { window.emit(sender, Message::Quit(false)); sender.send(Message::Themes(theme)); app::set_font(Font::Courier); - while app::App::default().load_system_fonts().wait() { + while app::App::default().with_scheme(app::Scheme::Base).wait() { match receiver.recv() { Some(Message::Quit(force)) => { if force || app::event() == Event::Close { @@ -152,15 +154,7 @@ fn main() { app::quit(); } } - Some(Message::Info) => { - let mut dialog = HelpDialog::default(); - dialog.set_value(INFO); - dialog.set_text_size(16); - dialog.show(); - while dialog.shown() { - app::wait(); - } - } + Some(Message::Info) => info(), Some(Message::Themes(ord)) => { theme = ord; window.set_color(COLORS[theme as usize][0]); @@ -302,6 +296,22 @@ fn button(title: &'static str) -> Button { element } +fn info() { + const INFO: &str = "

+FlCalculator + is similar to + Calculator + 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(); + } +} const SVG: &str = r#" @@ -327,14 +337,6 @@ const SVG: &str = r#" "#; -const INFO: &str = "

-FlCalculator - is similar to - Calculator - written using - FLTK-RS -

"; - #[derive(Copy, Clone)] enum Message { Info, @@ -350,7 +352,6 @@ enum Message { C, } -const CFG: &str = "/.config/flcalculator"; const SIZE: i32 = 25; const OUTPUT: i32 = 36; const THEMES: [&str; 2] = ["Light", "Dark"]; diff --git a/fldialect/Cargo.toml b/fldialect/Cargo.toml index 7fe71ca..0f1e1e7 100644 --- a/fldialect/Cargo.toml +++ b/fldialect/Cargo.toml @@ -1,6 +1,6 @@ [package] homepage = "https://github.com/fltk-rs" -authors = "Artem V. Ageev" +authors = ["Artem V. Ageev"] name = "fldialect" version = "0.0.1" edition = "2021" @@ -8,5 +8,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = { version = "^1.4" } +fltk = { version = "^1.4", features = ["use-ninja"] } fltk-theme = { version="0.7" } diff --git a/fldialect/src/commands/mod.rs b/fldialect/src/commands/mod.rs index 30ce0e4..0e8d99b 100644 --- a/fldialect/src/commands/mod.rs +++ b/fldialect/src/commands/mod.rs @@ -26,9 +26,8 @@ pub fn run(tts: bool, from: String, to: String, word: String) -> String { pub fn list() -> String { if cfg!(target_family = "unix") { - let run = Command::new("bash") - .arg("-xc") - .arg("trans -list-languages-english") + let run = Command::new("trans") + .arg("-list-languages-english") .output() .expect("failed to execute bash"); match run.status.success() { diff --git a/fldialect/src/components/footer.rs b/fldialect/src/components/footer.rs index 2a38c0c..cee1a40 100644 --- a/fldialect/src/components/footer.rs +++ b/fldialect/src/components/footer.rs @@ -30,7 +30,11 @@ pub struct Footer { impl Footer { pub fn build(flex: &mut Flex, font: i32, size: i32) -> Self { - let fonts = Vec::from(["Courier".to_string(), "Helvetica".to_string(), "Times".to_string()]); + let fonts = Vec::from([ + "Courier".to_string(), + "Helvetica".to_string(), + "Times".to_string(), + ]); let mut layout = Flex::default_fill(); let mut component = Self { layout: layout.clone(), diff --git a/fldialect/src/constants/mod.rs b/fldialect/src/constants/mod.rs index 8a31593..086627d 100644 --- a/fldialect/src/constants/mod.rs +++ b/fldialect/src/constants/mod.rs @@ -23,15 +23,14 @@ pub const PARAMS: [u8; 9] = [ ]; pub const CFG: &str = "/.config/fldialect"; pub const APPNAME: &str = "FlDialect"; -pub const INFO: &str = "

-FlDialect +pub const INFO: &str = r#"

+FlDialect is similar to - Dialect + Dialect written using - FLTK-RS - and translate-shell. -

"; - + FLTK-RS + and translate-shell. +

"#; pub const SVG: &str = r#" diff --git a/fldialect/src/pages/mod.rs b/fldialect/src/pages/mod.rs index 824a052..5160c5e 100644 --- a/fldialect/src/pages/mod.rs +++ b/fldialect/src/pages/mod.rs @@ -19,7 +19,18 @@ use { pub fn main() { let file = env::var("HOME").unwrap() + CFG; let params: Vec = match Path::new(&file).exists() { - true => fs::read(&file).unwrap(), + true => { + if let Ok(value) = fs::read(&file) { + if value.len() == PARAMS.len() { + value + } else { + fs::remove_file(&file).unwrap(); + Vec::from(PARAMS) + } + } else { + Vec::from(PARAMS) + } + } false => Vec::from(PARAMS), }; let mut window = Window::default() @@ -40,24 +51,6 @@ pub fn main() { window.make_resizable(true); window.show(); window.set_icon(Some(SvgImage::from_data(SVG).unwrap())); - run( - &mut window, - &mut flex, - &mut header, - &mut hero, - &mut footer, - &file, - ); -} - -fn run( - window: &mut Window, - flex: &mut Flex, - header: &mut Header, - hero: &mut Hero, - footer: &mut Footer, - file: &str, -) { let (sender, receiver) = app::channel::(); for (ord, item) in THEMES.iter().enumerate() { let idx = header.menu.add_emit( @@ -180,7 +173,7 @@ fn run( let width = window.width(); let height = window.height(); fs::write( - file, + &file, [ hero.theme, (width / U8) as u8, diff --git a/flmusic/Cargo.toml b/flmusic/Cargo.toml new file mode 100644 index 0000000..3ea2b13 --- /dev/null +++ b/flmusic/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "flmusic" +version = "0.0.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fltk = { version = "^1.4", features = ["use-ninja"] } +soloud = { version="^1.0" } diff --git a/flmusic/README.md b/flmusic/README.md new file mode 100644 index 0000000..53780ff --- /dev/null +++ b/flmusic/README.md @@ -0,0 +1,2 @@ +![FlMusic](assets/base.png "FlMusic") +![FlMusic](assets/base_dark.png "FlMusic") diff --git a/flmusic/assets/base.png b/flmusic/assets/base.png new file mode 100644 index 0000000000000000000000000000000000000000..404981e2313fa64f29c339d9482fe4e945aaab52 GIT binary patch literal 23523 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 K*6sfMvJ=_v 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 literal 0 HcmV?d00001 diff --git a/flmusic/src/components/mod.rs b/flmusic/src/components/mod.rs new file mode 100644 index 0000000..3059c67 --- /dev/null +++ b/flmusic/src/components/mod.rs @@ -0,0 +1,252 @@ +use { + crate::Message, + fltk::{ + app, + browser::{Browser, BrowserType}, + button::Button, + dialog::{choice2_default, FileChooser, FileChooserType}, + enums::{Event, Shortcut}, + group::Flex, + menu::{MenuButton, MenuButtonType, MenuFlag}, + prelude::{BrowserExt, GroupExt, MenuExt, ValuatorExt, WidgetBase, WidgetExt, WindowExt}, + valuator::{Scrollbar, ScrollbarType}, + window::Window, + }, + soloud::{audio::Wav, AudioExt, LoadExt, Soloud}, + std::{ffi::OsStr, fs, path::Path}, +}; + +pub struct Page { + model: Vec, + pub player: Soloud, + pub menu: MenuButton, + browser: Browser, + prev: Button, + play: Button, + stop: Button, + next: Button, + song: Scrollbar, + vol: Scrollbar, + rem: Button, +} + +impl Page { + pub fn build(sender: app::Sender) -> Self { + let mut menu = MenuButton::default().with_type(MenuButtonType::Popup3); + for (label, chr, msg) in [ + ("@#+2menu &Remove", 'd', Message::Remove), + ("@#+2+ &Add", 'n', Message::Plus), + ] { + menu.add_emit( + &format!("{label}\t"), + Shortcut::Ctrl | chr, + MenuFlag::Normal, + sender, + msg, + ); + } + let mut window = Window::default() + .with_size(640, 360) + .with_label("FlMusic") + .center_screen(); + let mut margin = Flex::default_fill().column(); + let mut header = Flex::default(); + let mut open = button("@#+2fileopen", "Open", &mut header); + open.emit(sender, Message::Open); + open.activate(); + let mut prev = button("@#+2|<", "Prev ...", &mut header); + prev.emit(sender, Message::Prev); + let mut play = button("@#+2>", "Play", &mut header); + play.emit(sender, Message::Play); + let group = Flex::default().column(); + let mut song = Scrollbar::default().with_type(ScrollbarType::HorizontalNice); + song.deactivate(); + let mut vol = Scrollbar::default().with_type(ScrollbarType::HorizontalNice); + vol.emit(sender, Message::Volume); + vol.set_range(0.0, 100.0); + vol.set_value(50.0); + vol.set_precision(0); + vol.deactivate(); + group.end(); + let mut stop = button("@#+1square", "Stop", &mut header); + stop.emit(sender, Message::Stop); + let mut next = button("@#+2>|", "Next", &mut header); + next.emit(sender, Message::Next); + let mut rem = button("@#+2menu", "Remove", &mut header); + rem.emit(sender, Message::Remove); + header.end(); + header.set_pad(0); + let mut browser = Browser::default().with_type(BrowserType::Hold); + margin.end(); + margin.fixed(&header, 30); + margin.set_margin(10); + margin.set_pad(10); + window.end(); + window.make_resizable(true); + window.show(); + window.emit(sender, Message::Quit(false)); + browser.handle(move |_, event| match event { + Event::Push => match app::event_mouse_button() { + app::MouseButton::Right => { + sender.send(Message::Select); + true + } + app::MouseButton::Left => true, + _ => false, + }, + _ => false, + }); + Self { + model: Vec::new(), + player: Soloud::default().expect("Cannot access audio backend"), + menu, + browser, + prev, + play, + stop, + next, + song, + vol, + rem, + } + } + pub fn update(&mut self) { + self.browser.clear(); + for item in &self.model { + self.browser.add(item); + } + self.browser.sort(); + self.browser.select(1); + app::redraw(); + } + pub fn plus(&mut self) { + let mut dialog = FileChooser::new( + std::env::var("HOME").unwrap(), + "*.{mp3}", + FileChooserType::Multi, + "Choose File...", + ); + dialog.show(); + while dialog.shown() { + app::wait(); + } + if dialog.count() > 0 { + for item in 1..=dialog.count() { + if let Some(file) = dialog.value(item) { + self.model.push(file); + self.update(); + }; + } + self.play.activate(); + self.vol.activate(); + }; + } + pub fn open(&mut self) { + let mut dialog = FileChooser::new( + std::env::var("HOME").unwrap(), + "", + FileChooserType::Directory, + "Choose File...", + ); + dialog.show(); + while dialog.shown() { + app::wait(); + } + if dialog.count() > 0 { + if let Some(file) = dialog.value(1) { + if let Ok(entries) = fs::read_dir(file) { + self.model = entries + .map(|entry| entry.ok().unwrap().path()) + .filter(|path| { + ["mp3"] + .map(|x| Some(OsStr::new(x))) + .contains(&path.extension()) + }) + .map(|path| format!("{}", path.display())) + .collect::>(); + }; + self.update(); + self.play.activate(); + self.vol.activate(); + self.next.activate(); + self.prev.activate(); + self.rem.activate(); + }; + }; + } + pub fn stop(&mut self) { + if self.player.active_voice_count() > 0 { + self.play.set_label("@#+2>"); + self.play.set_tooltip("Start"); + self.stop.deactivate(); + self.player.stop_all(); + } + } + pub fn play(&mut self) { + if !self.model.is_empty() { + if self.player.active_voice_count() > 0 { + self.play.set_label("@#+2>"); + self.play.set_tooltip("Start"); + self.stop.deactivate(); + self.player.stop_all(); + } else { + self.play.set_label("@#+2||"); + self.play.set_tooltip("Stop"); + self.stop.activate(); + let mut wav = Wav::default(); + if wav + .load(Path::new(&self.browser.text(self.browser.value()).unwrap())) + .is_ok() + { + let handle = self.player.play(&wav); + self.player.set_pause(handle, true); + self.player.set_volume(handle, self.vol.value() as f32); + self.song.set_range(0.0, wav.length()); + self.song.set_step(wav.length(), 20); + self.player.play(&wav); + while self.player.active_voice_count() > 0 { + app::wait(); + } + }; + } + } + } + pub fn volume(&mut self) { + eprintln!("{}", self.vol.value()); + self.player.set_global_volume(self.vol.value() as f32); + } + pub fn remove(&mut self) { + match choice2_default("Remove ...?", "Remove", "Cancel", "Permanent") { + Some(0) => { + if fs::remove_file(&self.model[self.browser.value() as usize - 1]).is_ok() { + self.model.remove(self.browser.value() as usize - 1); + self.update(); + } + } + Some(1) => {} + _ => {} + }; + } + pub fn next(&mut self) { + match self.browser.value() < self.browser.size() { + true => self.browser.select(self.browser.value() + 1), + false => self.browser.select(1), + }; + app::redraw(); + } + pub fn prev(&mut self) { + match self.browser.value() > 1 { + true => self.browser.select(self.browser.value() - 1), + false => self.browser.select(self.browser.size()), + }; + app::redraw(); + } +} + +pub fn button(label: &str, tooltip: &str, flex: &mut Flex) -> Button { + let mut element = Button::default().with_label(label); + element.set_tooltip(tooltip); + element.deactivate(); + flex.fixed(&element, 30); + element +} diff --git a/flmusic/src/main.rs b/flmusic/src/main.rs new file mode 100644 index 0000000..e6bfebc --- /dev/null +++ b/flmusic/src/main.rs @@ -0,0 +1,53 @@ +#![forbid(unsafe_code)] + +mod components; + +use { + crate::components::Page, + fltk::{ + app, + enums::{Event, Font}, + }, +}; + +#[derive(Clone, Copy)] +pub enum Message { + Quit(bool), + Plus, + Stop, + Next, + Prev, + Open, + Play, + Volume, + Remove, + Select, +} + +fn main() { + let (sender, receiver) = app::channel::(); + let mut page = Page::build(sender); + app::set_font(Font::Courier); + while app::App::default().with_scheme(app::Scheme::Plastic).wait() { + match receiver.recv() { + Some(Message::Select) => { + page.menu.popup(); + } + Some(Message::Remove) => page.remove(), + Some(Message::Plus) => page.plus(), + Some(Message::Open) => page.open(), + Some(Message::Play) => page.play(), + Some(Message::Stop) => page.stop(), + Some(Message::Next) => page.next(), + Some(Message::Prev) => page.prev(), + Some(Message::Volume) => page.volume(), + Some(Message::Quit(force)) => { + if force || app::event() == Event::Close { + page.player.stop_all(); + app::quit(); + } + } + None => {} + }; + } +} diff --git a/flpicture/Cargo.toml b/flpicture/Cargo.toml new file mode 100644 index 0000000..da7d8ca --- /dev/null +++ b/flpicture/Cargo.toml @@ -0,0 +1,11 @@ +[package] +homepage = "https://github.com/fltk-rs" +authors = ["Artem V. Ageev"] +name = "flpicture" +version = "0.0.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fltk = { version = "^1.4", features = ["use-ninja"] } diff --git a/flpicture/README.md b/flpicture/README.md new file mode 100644 index 0000000..5f0ada2 --- /dev/null +++ b/flpicture/README.md @@ -0,0 +1,2 @@ +![FlPicView](assets/base.png "FlPicView") +![FlPicView](assets/base_dark.png "FlPicView") diff --git a/flpicture/assets/base.png b/flpicture/assets/base.png new file mode 100644 index 0000000000000000000000000000000000000000..0ed0173edc413544fa154134d5c237f26a3c2948 GIT binary patch literal 21722 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 L8<$_casU4S{c@(p literal 0 HcmV?d00001 diff --git a/flpicture/assets/base_dark.png b/flpicture/assets/base_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..310bcb893753214ca2f8fab6e817e95b3e55d647 GIT binary patch literal 23797 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, +} + +impl Page { + pub fn build(sender: app::Sender) -> Self { + let mut menu = MenuButton::default().with_type(MenuButtonType::Popup3); + for (label, chr, msg) in [ + ("&File/@#fileopen &Open", 'o', Message::Open(true)), + ("&File/@#menu &Remove", 'p', Message::Remove), + ("@#1+ &Quit", 'q', Message::Quit(true)), + ] { + menu.add_emit( + &format!("{label}\t"), + Shortcut::Ctrl | chr, + MenuFlag::Normal, + sender, + msg, + ); + } + let mut window = Window::default() + .with_size(640, 360) + .with_label("FlPicView") + .center_screen(); + let mut margin = Flex::default_fill().column(); + let mut footer = Flex::default(); + let mut open = button("@#+2fileopen", "Open ...", &mut footer); + open.activate(); + open.emit(sender, Message::Open(true)); + let mut prev = button("@#+2<-", "Previous", &mut footer); + prev.emit(sender, Message::Prev); + let mut size = Scrollbar::default().with_type(ScrollbarType::HorizontalNice); + size.emit(sender, Message::Size); + size.set_range(0f64, 100f64); + size.set_precision(0); + size.set_value(50f64); + size.deactivate(); + let mut next = button("@#+2->", "Next", &mut footer); + next.emit(sender, Message::Next); + let mut rem = button("@#+2menu", "Remove", &mut footer); + rem.emit(sender, Message::Remove); + footer.end(); + footer.set_pad(0); + let mut image = Frame::default_fill(); + image.set_image(None::); + image.handle(move |_, event| match event { + Event::Push => match app::event_mouse_button() { + app::MouseButton::Right => { + menu.popup(); + true + } + _ => false, + }, + _ => false, + }); + margin.end(); + margin.fixed(&footer, 30); + margin.set_margin(10); + window.end(); + window.make_resizable(true); + window.show(); + window.emit(sender, Message::Quit(false)); + Self { + window, + open, + prev, + size, + next, + rem, + image, + pos: 0, + images: Vec::new(), + } + } + pub fn set(&mut self) { + if !self.images.is_empty() { + if let Ok(mut image) = SharedImage::load(&self.images[self.pos]) { + image.scale( + (self.image.width() as f64 * self.size.value()) as i32 / 100, + (self.image.height() as f64 * self.size.value()) as i32 / 100, + true, + true, + ); + self.image.set_image(Some(image)); + self.window.set_label(&format!( + "{} - FlPicView ({}%)", + self.images[self.pos], + self.size.value() + )); + }; + self.prev.activate(); + self.next.activate(); + self.rem.activate(); + self.size.activate(); + } else { + self.prev.deactivate(); + self.next.deactivate(); + self.rem.deactivate(); + self.size.deactivate(); + }; + app::redraw(); + } + pub fn open(&mut self, ui: bool) { + if ui { + let mut dialog = FileChooser::new( + std::env::var("HOME").unwrap(), + "*.{png,svg}", + FileChooserType::Single, + "Choose File...", + ); + dialog.show(); + while dialog.shown() { + app::wait(); + } + if dialog.count() > 0 { + if let Some(file) = dialog.value(1) { + self.load(&file); + self.set(); + }; + }; + } else { + let args: Vec = env::args().collect(); + if args.len() > 1 { + self.load(&args[1]); + self.set(); + }; + }; + } + pub fn load(&mut self, file: &str) { + if let Some(parent) = Path::new(file).parent() { + if let Ok(entries) = fs::read_dir(parent) { + self.images = entries + .map(|entry| entry.ok().unwrap().path()) + .filter(|path| { + ["png", "svg"] + .map(|x| Some(OsStr::new(x))) + .contains(&path.extension()) + }) + .map(|path| format!("{}", path.display())) + .collect::>(); + self.pos = self + .images + .iter() + .position(|item| item == file) + .unwrap_or(0); + eprintln!("{:?}\n{}", self.images, self.pos); + } + }; + } + pub fn next(&mut self) { + match self.pos < self.images.len() - 1 { + true => self.pos += 1, + false => self.pos = 0, + }; + self.set(); + } + pub fn prev(&mut self) { + match self.pos > 0 { + true => self.pos -= 1, + false => self.pos = self.images.len() - 1, + }; + self.set(); + } + pub fn rem(&mut self) { + if let Some(0) = choice2_default( + &format!("Remove {}?", self.images[self.pos]), + "Remove", + "Cancel", + "", + ) { + if fs::remove_file(&self.images[self.pos]).is_ok() { + self.images.remove(self.pos); + match self.images.is_empty() { + false => self.next(), + true => { + self.image.set_image(None::); + self.prev.deactivate(); + self.next.deactivate(); + self.rem.deactivate(); + } + }; + }; + }; + } +} + +fn button(label: &str, tooltip: &str, layout: &mut Flex) -> Button { + let mut element = Button::default().with_label(label); + layout.fixed(&element, 30); + element.set_tooltip(tooltip); + element.deactivate(); + element +} diff --git a/flpicture/src/main.rs b/flpicture/src/main.rs new file mode 100644 index 0000000..84c9ba5 --- /dev/null +++ b/flpicture/src/main.rs @@ -0,0 +1,40 @@ +#![forbid(unsafe_code)] + +mod components; + +use fltk::{ + app, + enums::{Event, Font}, +}; + +#[derive(Clone, Copy)] +pub enum Message { + Quit(bool), + Open(bool), + Prev, + Next, + Size, + Remove, +} + +fn main() { + let (sender, receiver) = app::channel::(); + let mut page = components::Page::build(sender); + sender.send(Message::Open(false)); + app::set_font(Font::Courier); + while app::App::default().with_scheme(app::Scheme::Plastic).wait() { + match receiver.recv() { + Some(Message::Open(ui)) => page.open(ui), + Some(Message::Size) => page.set(), + Some(Message::Next) => page.next(), + Some(Message::Prev) => page.prev(), + Some(Message::Remove) => page.rem(), + Some(Message::Quit(force)) => { + if force || app::event() == Event::Close { + app::quit(); + } + } + None => {} + }; + } +} diff --git a/flresters/Cargo.toml b/flresters/Cargo.toml new file mode 100644 index 0000000..ae6e507 --- /dev/null +++ b/flresters/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "flresters" +version = "0.1.1" +edition = "2021" +description = "A lightweight cross-platform Rest API tester gui built using fltk-rs" +repository = "https://github.com/MoAlyousef/resters" +license = "MIT" +categories = ["visualization"] +documentation = "https://docs.rs/resters" +readme = "README.md" +keywords = ["cross-platform", "rest", "api", "gui", "http"] + +[dependencies] +fltk = { version = "^1.4", features = ["use-ninja"] } +fltk-theme = { version="^0.7" } +ureq = { version = "2.5.0", features = ["json"] } +serde_json = "1" +json-tools = "1.1" diff --git a/flresters/LICENSE b/flresters/LICENSE new file mode 100644 index 0000000..6802bc4 --- /dev/null +++ b/flresters/LICENSE @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/flresters/README.md b/flresters/README.md new file mode 100644 index 0000000..8c743d7 --- /dev/null +++ b/flresters/README.md @@ -0,0 +1,2 @@ +![flResters](assets/base_dark_linux.png "flResters") +![flResters](assets/full_dark_linux.png "flResters") diff --git a/flresters/assets/base_dark_linux.png b/flresters/assets/base_dark_linux.png new file mode 100644 index 0000000000000000000000000000000000000000..63fd838c6161c8cabf3f61ae2b3caa817983c7ff GIT binary patch literal 8860 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 Ji)2iL{ukF|s51Zn literal 0 HcmV?d00001 diff --git a/flresters/src/main.rs b/flresters/src/main.rs new file mode 100644 index 0000000..aef7996 --- /dev/null +++ b/flresters/src/main.rs @@ -0,0 +1,142 @@ +#![forbid(unsafe_code)] +use { + fltk::{enums::*, prelude::*, *}, + fltk_theme::{color_themes, ColorTheme}, + json_tools::{Buffer, BufferType, Lexer, Span, TokenType}, + ureq::Error, +}; + +fn main() { + let mut sbuf = text::TextBuffer::default(); + let styles: Vec = [0xdc322f, 0x268bd2, 0x859900] + .into_iter() + .map(|color| text::StyleTableEntry { + color: Color::from_hex(color), + font: Font::Courier, + size: 16, + }) + .collect(); + let mut window = window::Window::default() + .with_size(640, 360) + .with_label("flResters") + .center_screen(); + window.set_xclass("resters"); + let mut flex = group::Flex::default_fill().column(); + let mut header = group::Flex::default(); + flex.fixed(&header, 30); + let mut choice = menu::Choice::default(); + choice.add_choice("GET|POST"); + choice.set_value(0); + header.fixed(&choice, 80); + header.fixed(&frame::Frame::default().with_label("https://"), 60); + let mut inp = input::Input::default(); + inp.set_trigger(CallbackTrigger::EnterKeyAlways); + let mut info = button::Button::default().with_label("ℹ️"); + info.set_label_size(18); + info.set_frame(FrameType::NoBox); + info.set_callback(move |_| { + dialog::message_default("Resters was created using Rust and fltk-rs. It is MIT licensed!") + }); + header.fixed(&info, 30); + header.end(); + header.set_pad(10); + let mut disp = text::TextDisplay::default(); + disp.wrap_mode(text::WrapMode::AtBounds, 0); + disp.set_buffer(text::TextBuffer::default()); + disp.set_color(Color::from_hex(0x002b36)); + disp.set_highlight_data(sbuf.clone(), styles); + let mut footer = group::Flex::default(); + flex.fixed(&footer, 20); + footer.fixed(&frame::Frame::default().with_label("Status: "), 80); + let mut status = frame::Frame::default().with_align(Align::Left | Align::Inside); + footer.end(); + flex.end(); + flex.set_pad(10); + flex.set_margin(10); + window.end(); + window.make_resizable(true); + window.show(); + window.set_icon(Some(image::SvgImage::from_data(SVG).unwrap())); + inp.set_callback(move |inp| { + status.set_label(""); + disp.buffer().unwrap().set_text(""); + sbuf.set_text(""); + let mut path = inp.value(); + if !path.starts_with("https://") { + path = String::from("https://") + &path; + } + let req = match choice.value() { + 0 => ureq::get(&path), + 1 => ureq::post(&path), + _ => unreachable!(), + }; + match req.call() { + Ok(response) => { + if let Ok(json) = response.into_json::() { + let json: String = serde_json::to_string_pretty(&json).unwrap(); + disp.buffer().unwrap().set_text(&json); + fill_style_buffer(&mut sbuf, &json); + status.set_label("200 OK"); + status.set_label_color(enums::Color::Yellow); + } else { + dialog::message_default("Error parsing json"); + } + } + Err(Error::Status(code, response)) => { + status.set_label(&format!("{} {}", code, response.status_text())); + status.set_label_color(enums::Color::Red); + } + Err(e) => { + dialog::message_default(&e.to_string()); + } + } + }); + ColorTheme::new(color_themes::DARK_THEME).apply(); + app::set_scheme(app::Scheme::Plastic); + app::set_font(Font::Courier); + app::App::default().run().unwrap(); +} + +fn fill_style_buffer(sbuf: &mut text::TextBuffer, s: &str) { + let mut local_buf = vec![b'A'; s.len()]; + for token in Lexer::new(s.bytes(), BufferType::Span) { + use TokenType::*; + let c = match token.kind { + CurlyOpen | CurlyClose | BracketOpen | BracketClose | Colon | Comma | Invalid => 'A', + String => 'B', + BooleanTrue | BooleanFalse | Null => 'C', + Number => 'D', + }; + if let Buffer::Span(Span { first, end }) = token.buf { + let start = first as _; + let last = end as _; + local_buf[start..last].copy_from_slice(c.to_string().repeat(last - start).as_bytes()); + } + } + sbuf.set_text(&String::from_utf8_lossy(&local_buf)); +} + +const SVG: &str = r#" + + + + + image/svg+xml + + + + + + + + + + + + + + + + + +"#; diff --git a/fltext/Cargo.toml b/fltext/Cargo.toml new file mode 100644 index 0000000..706f228 --- /dev/null +++ b/fltext/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "fltext" +version = "0.1.0" +edition = "2021" + +[dependencies] +fltk = "^1.4" +fltk-theme = "0.7" +regex = "1.9.6" +notify = "5.1" +# optional deps +# term +fltk-term = { version = "0.1", optional = true } +# highlight +tree-sitter-highlight = { version = "0.20", optional = true } +tree-sitter-rust = { version = "0.20", optional = true } +tree-sitter-toml = { version = "0.20", optional = true } +tree-sitter-md = { version = "0.1", optional = true } + +[features] +default = ["highlight", "term"] +highlight = [ + "tree-sitter-highlight", + "tree-sitter-rust", + "tree-sitter-toml", + "tree-sitter-md", + ] +term = [ + "fltk-term" +] diff --git a/fltext/LICENSE b/fltext/LICENSE new file mode 100644 index 0000000..93a4574 --- /dev/null +++ b/fltext/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Mohammed Alyousef + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/fltext/README.md b/fltext/README.md new file mode 100644 index 0000000..1bcf9b9 --- /dev/null +++ b/fltext/README.md @@ -0,0 +1,34 @@ +# red + +red or the RustyEditor is a lightweight and minimal text editor which supports multiple-tabs (à la vscode) and which integrates a file browser and a terminal. The editor component supports syntax highlighting via [tree-sitter-highlight](https://github.com/tree-sitter/tree-sitter/tree/master/highlight). + +## Building +```bash +git clone https://github.com/MoAlyousef/red +cd red +cargo build --release +``` + +To disable building with tree-sitter and the terminal: +```bash +cargo build --no-default-features --release +``` + +To build with native wayland support on Linux: +```bash +cargo build --features=fltk/use-wayland --release +``` + +![image](https://github.com/MoAlyousef/red/assets/37966791/c43a180f-d1db-4528-ace6-d3713dcda202) + +## Known issues +- If you're running KDE and no icons appear in the FileBrowser, you can try setting the KDEDIR to /usr/local. +- Highlighting via tree-sitter seems to vary between different language modules. tree-sitter-json seems quite limited for example. + +## ToDo +- Add a settings dialog. +- Save and get user settings using fltk [Preferences](https://docs.rs/fltk/latest/fltk/app/prefs/struct.Preferences.html). +- Enable using FLTK's FileChooser instead of the system provided one via the settings. +- Add more options to FileBrowser popup menu and the terminal menu. +- Support user provided color schemes for the app and the highlighting. +- Support regex for search & replace. diff --git a/fltext/src/cbs.rs b/fltext/src/cbs.rs new file mode 100644 index 0000000..1dec330 --- /dev/null +++ b/fltext/src/cbs.rs @@ -0,0 +1,222 @@ +use crate::state::STATE; +use fltk::{enums::*, prelude::*, *}; +use std::{fs, path::PathBuf}; + +fn nfc_get_file(mode: dialog::NativeFileChooserType) -> PathBuf { + let mut nfc = dialog::NativeFileChooser::new(mode); + nfc.show(); + nfc.filename() +} + +fn find() { + let mut dlg: window::Window = app::widget_from_id("find").unwrap(); + let main_win = app::first_window().unwrap(); + dlg.resize(main_win.x() + main_win.w() - 300, dlg.y() + 30, 300, 50); + dlg.show(); +} + +fn replace() { + let mut dlg: window::Window = app::widget_from_id("replace").unwrap(); + let main_win = app::first_window().unwrap(); + dlg.resize(main_win.x() + main_win.w() - 300, dlg.y() + 30, 300, 80); + dlg.show(); +} + +pub fn win_cb(_: &mut window::Window) { + if app::event() == Event::Close { + app::quit(); + } +} + +pub fn editor_cb(_e: &mut text::TextEditor) { + app::add_timeout3(0.01, |_| STATE.with(|s| s.was_modified(true))); +} + +pub fn new_file() { + let dlg = dialog::input_default("Enter file name", ""); + if let Some(f) = dlg { + fs::File::create(f).ok(); + } +} + +pub fn new_dir() { + let dlg = dialog::input_default("Enter directory name", ""); + if let Some(f) = dlg { + fs::create_dir(f).ok(); + } +} + +pub fn menu_cb(m: &mut impl MenuExt) { + if let Ok(mpath) = m.item_pathname(None) { + match mpath.as_str() { + "&File/New File...\t" => { + new_file(); + } + "&File/New Dir...\t" => { + new_dir(); + } + "&File/Open...\t" => { + let c = nfc_get_file(dialog::NativeFileChooserType::BrowseFile); + if c.exists() { + STATE.with(move |s| { + s.append(Some(c.canonicalize().unwrap())); + }); + } + } + "&File/Save\t" => { + STATE.with(|s| { + if let Some(id) = s.current_id() { + let e = s.map.get(&id).unwrap(); + let modified = e.modified; + if let Some(current_file) = e.current_file.as_ref() { + if modified && current_file.exists() { + fs::write(current_file, e.buf.text()).ok(); + s.was_modified(false); + } + } + } + }); + } + "&File/Save as...\t" => { + let c = nfc_get_file(dialog::NativeFileChooserType::BrowseSaveFile); + if c.exists() { + STATE.with(move |s| { + if let Some(buf) = s.buf().as_ref() { + fs::write(&c, buf.text()).expect("Failed to write to file!"); + s.was_modified(false); + } + }); + } + } + "&File/Save All\t" => { + STATE.with(|s| { + for v in s.map.values_mut() { + if v.modified && v.current_file.as_ref().unwrap().exists() { + fs::write(v.current_file.as_ref().unwrap(), v.buf.text()).ok(); + v.modified = true; + } + } + }); + } + "&File/Quit\t" => app::quit(), + "/Undo\t" | "&Edit/Undo\t" => STATE.with(|s| { + if let Some(e) = s.current_editor() { + e.undo() + } + }), + "/Redo\t" | "&Edit/Redo\t" => STATE.with(|s| { + if let Some(e) = s.current_editor() { + e.redo() + } + }), + "/Cut\t" | "&Edit/Cut\t" => STATE.with(|s| { + if let Some(e) = s.current_editor() { + e.cut() + } + }), + "/Copy\t" | "&Edit/Copy\t" => STATE.with(|s| { + if let Some(e) = s.current_editor() { + e.copy() + } + }), + "/Paste\t" | "&Edit/Paste\t" => STATE.with(|s| { + if let Some(e) = s.current_editor() { + e.paste() + } + }), + "/Find\t" | "&Edit/Find\t" => find(), + "/Replace\t" | "&Edit/Replace\t" => replace(), + "&View/File browser\t" => { + let mut item = m.at(m.value()).unwrap(); + let fbr: group::Group = app::widget_from_id("fbr_group").unwrap(); + let mut parent = group::Flex::from_dyn_widget(&fbr.parent().unwrap()).unwrap(); + if !item.value() { + parent.fixed(&fbr, 1); + item.clear(); + } else { + parent.fixed(&fbr, 180); + item.set(); + } + app::redraw(); + } + "&View/Terminal\t" => { + let mut item = m.at(m.value()).unwrap(); + let term: group::Group = app::widget_from_id("term_group").unwrap(); + let mut parent = group::Flex::from_dyn_widget(&term.parent().unwrap()).unwrap(); + if !item.value() { + parent.fixed(&term, 1); + item.clear(); + } else { + parent.fixed(&term, 160); + item.set(); + } + app::redraw(); + } + "&Help/About\t" => { + dialog::message_title("About"); + dialog::message_default("A minimal text editor written using fltk-rs!") + } + _ => unreachable!(), + } + } +} + +pub fn tab_close_cb(g: &mut impl GroupExt) { + if app::callback_reason() == CallbackReason::Closed { + let ed = text::TextEditor::from_dyn_widget(&g.child(0).unwrap()).unwrap(); + let edid = ed.as_widget_ptr() as usize; + let buf = ed.buffer().unwrap(); + let mut parent = g.parent().unwrap(); + parent.remove(g); + unsafe { + text::TextBuffer::delete(buf); + } + STATE.with(move |s| s.map.remove(&edid)); + parent.set_damage(true); + } +} + +#[cfg(feature = "term")] +pub fn tab_splitter_cb(f: &mut frame::Frame, ev: Event) -> bool { + let mut parent = group::Flex::from_dyn_widget(&f.parent().unwrap()).unwrap(); + let term = app::widget_from_id::("term_group").unwrap(); + match ev { + Event::Push => true, + Event::Drag => { + parent.fixed(&term, parent.h() + parent.y() - app::event_y()); + app::redraw(); + true + } + Event::Enter => { + f.window().unwrap().set_cursor(Cursor::NS); + true + } + Event::Leave => { + f.window().unwrap().set_cursor(Cursor::Arrow); + true + } + _ => false, + } +} + +pub fn fbr_splitter_cb(f: &mut frame::Frame, ev: Event) -> bool { + let mut parent = group::Flex::from_dyn_widget(&f.parent().unwrap()).unwrap(); + let fbr: group::Group = app::widget_from_id("fbr_group").unwrap(); + match ev { + Event::Push => true, + Event::Drag => { + parent.fixed(&fbr, app::event_x()); + app::redraw(); + true + } + Event::Enter => { + f.window().unwrap().set_cursor(Cursor::WE); + true + } + Event::Leave => { + f.window().unwrap().set_cursor(Cursor::Arrow); + true + } + _ => false, + } +} diff --git a/fltext/src/dialogs.rs b/fltext/src/dialogs.rs new file mode 100644 index 0000000..7608bf1 --- /dev/null +++ b/fltext/src/dialogs.rs @@ -0,0 +1,199 @@ +#![allow(dead_code)] + +use crate::state::STATE; +use fltk::{prelude::*, *}; +use std::cell::RefCell; +use std::rc::Rc; + +pub struct FindDialog { + win: window::Window, +} + +impl FindDialog { + pub fn new() -> Self { + let idx = Rc::from(RefCell::from(0)); + let mut win = window::Window::new(0, 0, 300, 50, "Find").with_id("find"); + win.set_border(false); + let mut row = group::Flex::default_fill(); + row.set_margin(10); + let f = frame::Frame::default().with_label("Find:"); + row.fixed(&f, 30); + let mut i = input::Input::default(); + i.set_trigger(enums::CallbackTrigger::EnterKeyAlways); + let mut reg = button::ToggleButton::default().with_label(".*"); + reg.set_selection_color(reg.color().lighter()); + reg.set_tooltip("Use regex"); + row.fixed(®, 30); + let mut b = button::Button::default().with_label("Next"); + i.set_callback({ + move |i| { + let val = i.value(); + let reg_val = reg.value(); + if reg_val && regex::Regex::new(&val).is_err() { + i.set_text_color(enums::Color::Red); + return; + } else { + i.set_text_color(enums::Color::Foreground); + } + if !val.is_empty() { + STATE.with({ + let idx = idx.clone(); + move |s| { + if let Some(buf) = s.buf().as_mut() { + let text = buf.text(); + if reg_val { + if let Ok(re) = regex::Regex::new(&val) { + let v: Vec<_> = + re.find_iter(&text).map(|m| m.range()).collect(); + if !v.is_empty() { + let mut idx = idx.borrow_mut(); + let curr = &v[*idx]; + let mut ed: text::TextEditor = + s.current_editor().unwrap(); + buf.select(curr.start as i32, curr.end as i32); + ed.scroll( + ed.count_lines(0, curr.start as i32, true), + 0, + ); + *idx += 1; + if *idx == v.len() { + *idx = 0; + } + } + } + } else { + let v: Vec<_> = text.match_indices(&val).collect(); + if !v.is_empty() { + let mut idx = idx.borrow_mut(); + let curr = v[*idx]; + let mut ed: text::TextEditor = s.current_editor().unwrap(); + buf.select(curr.0 as i32, (curr.0 + val.len()) as i32); + ed.scroll(ed.count_lines(0, curr.0 as i32, true), 0); + *idx += 1; + if *idx == v.len() { + *idx = 0; + } + } + } + } + } + }); + } + } + }); + b.set_callback(move |_| i.do_callback()); + row.fixed(&b, 60); + row.end(); + win.end(); + win.handle(|win, ev| match ev { + enums::Event::Hide => { + win.hide(); + true + } + enums::Event::Close => { + win.hide(); + true + } + _ => false, + }); + Self { win } + } +} + +pub struct ReplaceDialog { + win: window::Window, +} + +impl ReplaceDialog { + pub fn new() -> Self { + let mut win = window::Window::new(0, 0, 300, 80, "Replace").with_id("replace"); + win.set_border(false); + let mut col = group::Flex::default_fill().column(); + col.set_margin(5); + let mut row = group::Flex::default(); + let f = frame::Frame::default().with_label("Search:"); + row.fixed(&f, 60); + let mut search = input::Input::default(); + search.set_trigger(enums::CallbackTrigger::Changed); + let mut reg = button::ToggleButton::default().with_label(".*"); + reg.set_selection_color(reg.color().lighter()); + reg.set_tooltip("Use regex"); + row.fixed(®, 30); + row.end(); + let mut row = group::Flex::default(); + let f = frame::Frame::default().with_label("Replace:"); + row.fixed(&f, 60); + let replace = input::Input::default(); + let mut b = button::Button::default().with_label("@>"); + b.set_tooltip("Apply"); + row.fixed(&b, 30); + row.end(); + col.end(); + win.end(); + search.set_callback({ + let reg = reg.clone(); + move |i| { + let val = i.value(); + let reg_val = reg.value(); + if reg_val && regex::Regex::new(&val).is_err() { + i.set_text_color(enums::Color::Red); + } else { + i.set_text_color(enums::Color::Foreground); + } + } + }); + b.set_callback(move |_| { + let search = search.value(); + let replace = replace.value(); + let reg_val = reg.value(); + if reg_val && regex::Regex::new(&search).is_err() { + return; + } + STATE.with({ + move |s| { + if let Some(buf) = s.buf().as_mut() { + let text = buf.text(); + if reg_val { + if let Ok(re) = regex::Regex::new(&search) { + let ntext = re.replace(&text, &replace); + buf.set_text(&ntext); + } + } else { + let ntext = text.replace(&search, &replace); + buf.set_text(&ntext); + } + s.was_modified(true); + } + } + }); + }); + win.handle(|win, ev| match ev { + enums::Event::Hide => { + win.hide(); + true + } + enums::Event::Close => { + win.hide(); + true + } + _ => false, + }); + Self { win } + } +} + +pub struct ImageDialog { + win: window::Window, +} + +impl ImageDialog { + pub fn new() -> Self { + let mut win = window::Window::default() + .with_size(400, 300) + .with_id("image_dialog"); + let mut f = frame::Frame::default_fill(); + win.end(); + win.resize_callback(move |_win, _, _, w, h| f.resize(0, 0, w, h)); + Self { win } + } +} diff --git a/fltext/src/fbr.rs b/fltext/src/fbr.rs new file mode 100644 index 0000000..34e49f1 --- /dev/null +++ b/fltext/src/fbr.rs @@ -0,0 +1,128 @@ +#![allow(clippy::single_match)] + +use crate::{cbs, state::STATE, utils}; +use fltk::{enums::*, prelude::*, *}; +use notify::{event::EventKind, Event, RecursiveMode, Watcher}; +use std::{ + env, + path::{Path, PathBuf}, +}; + +pub fn menu_cb(m: &mut impl MenuExt) { + if let Ok(mpath) = m.item_pathname(None) { + match mpath.as_str() { + "/New File...\t" => cbs::new_file(), + "/New Dir...\t" => cbs::new_dir(), + _ => (), + } + } +} + +pub fn init_menu(m: &mut (impl MenuExt + 'static)) { + m.add( + "New File...\t", + Shortcut::Ctrl | 'n', + menu::MenuFlag::Normal, + menu_cb, + ); + m.add( + "New Dir...\t", + Shortcut::Ctrl | Shortcut::Shift | 'n', + menu::MenuFlag::Normal, + menu_cb, + ); +} + +pub fn fbr_cb(f: &mut browser::FileBrowser, watcher: &mut dyn Watcher) { + if let Some(path) = f.text(f.value()) { + let path = PathBuf::from(path); + if path.exists() { + if path.is_dir() { + watcher.watch(&path, RecursiveMode::NonRecursive).unwrap(); + f.load(&path).expect("Couldn't load directory!"); + let cwd = env::current_dir().unwrap(); + env::set_current_dir(cwd.join(path)).unwrap(); + let mut info: frame::Frame = app::widget_from_id("info").unwrap(); + info.set_label(&format!( + "Directory: {}", + utils::strip_unc_path(&env::current_dir().unwrap()) + )); + f.set_damage(true); + } else { + let mut is_image = false; + if let Some(ext) = path.extension() { + match ext.to_str().unwrap() { + "jpg" | "gif" | "png" | "bmp" => is_image = true, + _ => (), + } + } + if is_image { + let img = image::SharedImage::load(path).unwrap(); + let mut win: window::Window = app::widget_from_id("image_dialog").unwrap(); + win.resize(win.x(), win.y(), img.w(), img.h()); + win.child(0).unwrap().set_image(Some(img)); + win.show(); + } else { + STATE.with(move |s| { + s.append(Some(path.canonicalize().unwrap())); + }); + } + } + } + } +} + +pub struct Fbr { + g: group::Group, +} + +impl Fbr { + pub fn new(current_path: &Path) -> Self { + let mut g = group::Group::default().with_id("fbr_group"); + let mut fbr = browser::FileBrowser::default() + .with_type(browser::BrowserType::Hold) + .with_id("fbr"); + fbr.load(current_path) + .expect("Failed to load working directory"); + fbr.set_color(Color::Background.darker()); + let mut m = menu::MenuButton::default() + .with_type(menu::MenuButtonType::Popup3) + .with_id("pop1"); + init_menu(&mut m); + g.end(); + let mut watcher = notify::recommended_watcher({ + let mut fbr = fbr.clone(); + move |res: Result| match res { + Ok(event) => { + let mut needs_update = false; + match event.kind { + EventKind::Create(_) => { + needs_update = true; + } + EventKind::Remove(_) => { + needs_update = true; + } + _ => (), + } + if needs_update { + fbr.load(env::current_dir().unwrap()).unwrap(); + } + } + Err(e) => eprintln!("{}", e), + } + }) + .unwrap(); + watcher + .watch(current_path, RecursiveMode::NonRecursive) + .unwrap(); + fbr.set_callback(move |f| fbr_cb(f, &mut watcher)); + g.resize_callback(move |_, x, y, w, h| { + m.resize(x, y, w, h); + fbr.resize(x, y, w, h); + }); + + Self { g } + } +} + +fltk::widget_extends!(Fbr, group::Group, g); diff --git a/fltext/src/gui.rs b/fltext/src/gui.rs new file mode 100644 index 0000000..9c09820 --- /dev/null +++ b/fltext/src/gui.rs @@ -0,0 +1,269 @@ +use crate::{cbs, dialogs, fbr, utils}; +use fltk::{enums::*, prelude::*, *}; +use fltk_theme::{color_themes, ColorTheme}; +use std::path::{Path, PathBuf}; + +#[cfg(feature = "term")] +use fltk_term as term; + +#[cfg(feature = "highlight")] +use crate::highlight; + +const WIDTH: i32 = 800; +const HEIGHT: i32 = 600; +const MENU_HEIGHT: i32 = if cfg!(target_os = "macos") { 1 } else { 30 }; + +pub fn init_gui(current_file: &Option, current_path: &Path) -> app::App { + ColorTheme::new(color_themes::DARK_THEME).apply(); + app::set_scheme(app::Scheme::Plastic); + app::set_font(Font::Courier); + app::set_menu_linespacing(10); + let mut buf = text::TextBuffer::default(); + buf.set_tab_distance(4); + + let _find_dialog = dialogs::FindDialog::new(); + let _replace_dialog = dialogs::ReplaceDialog::new(); + let _image_dialog = dialogs::ImageDialog::new(); + + let mut popup = menu::MenuButton::default().with_type(menu::MenuButtonType::Popup3); + init_edit_menu(&mut popup, ""); + + let mut w = window::Window::default() + .with_size(WIDTH, HEIGHT) + .with_label("flText"); + w.set_xclass("red"); + + let mut col0 = group::Flex::default_fill().column(); + col0.set_pad(2); + let mut m = menu::SysMenuBar::default().with_id("menu"); + init_menu(&mut m, current_file.is_none()); + col0.fixed(&m, MENU_HEIGHT); + let mut row = group::Flex::default(); + row.set_pad(0); + let fbr = fbr::Fbr::new(current_path); + if current_file.is_none() { + row.fixed(&*fbr, 180); + } else { + row.fixed(&*fbr, 1); + } + let mut fbr_splitter = frame::Frame::default(); + fbr_splitter.handle(cbs::fbr_splitter_cb); + row.fixed(&fbr_splitter, 4); + let mut col = group::Flex::default().column(); + col.set_pad(0); + let mut tabs = group::Tabs::default().with_id("tabs"); + tabs.handle(move |t, ev| tabs_handle(t, ev, &mut popup)); + tabs.handle_overflow(group::TabsOverflow::Pulldown); + tabs.end(); + tabs.auto_layout(); + #[cfg(feature = "term")] + { + let mut tab_splitter = frame::Frame::default(); + tab_splitter.handle(cbs::tab_splitter_cb); + col.fixed(&tab_splitter, 4); + let term = term::PPTerm::default(); + col.fixed(&*term, 160); + } + col.end(); + row.end(); + let info = frame::Frame::default() + .with_label(&format!( + "Directory: {}", + utils::strip_unc_path(current_path) + )) + .with_align(enums::Align::Left | enums::Align::Inside) + .with_id("info"); + col0.fixed(&info, 20); + col0.end(); + w.resizable(&row); + w.end(); + w.make_resizable(true); + w.show(); + w.set_callback(cbs::win_cb); + app::App::default() +} + +pub fn tabs_handle(t: &mut group::Tabs, ev: Event, popup: &mut menu::MenuButton) -> bool { + match ev { + Event::Push => { + if app::event_mouse_button() == app::MouseButton::Right + && app::event_y() > t.y() + 30 + && t.children() > 0 + { + popup.popup(); + true + } else { + false + } + } + _ => false, + } +} + +pub fn init_edit_menu(m: &mut (impl MenuExt + 'static), header: &str) { + m.add( + &format!("{}Undo\t", header), + Shortcut::Ctrl | 'z', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); + m.add( + &format!("{}Redo\t", header), + Shortcut::Ctrl | 'y', + menu::MenuFlag::MenuDivider, + cbs::menu_cb, + ); + m.add( + &format!("{}Cut\t", header), + Shortcut::Ctrl | 'x', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); + m.add( + &format!("{}Copy\t", header), + Shortcut::Ctrl | 'c', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); + m.add( + &format!("{}Paste\t", header), + Shortcut::Ctrl | 'v', + menu::MenuFlag::MenuDivider, + cbs::menu_cb, + ); + m.add( + &format!("{}Find\t", header), + Shortcut::Ctrl | 'f', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); + m.add( + &format!("{}Replace\t", header), + Shortcut::Ctrl | 'h', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); +} +pub fn init_menu(m: &mut (impl MenuExt + 'static), load_dir: bool) { + m.add( + "&File/New File...\t", + Shortcut::Ctrl | 'n', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); + m.add( + "&File/New Dir...\t", + Shortcut::Ctrl | Shortcut::Shift | 'n', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); + m.add( + "&File/Open...\t", + Shortcut::Ctrl | 'o', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); + m.add( + "&File/Save\t", + Shortcut::Ctrl | 's', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); + m.add( + "&File/Save as...\t", + Shortcut::Ctrl | Shortcut::Shift | 'w', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); + m.add( + "&File/Save All\t", + Shortcut::None, + menu::MenuFlag::MenuDivider, + cbs::menu_cb, + ); + let idx = m.add( + "&File/Quit\t", + Shortcut::Ctrl | 'q', + menu::MenuFlag::Normal, + cbs::menu_cb, + ); + m.at(idx) + .unwrap() + .set_label_color(Color::from_hex(0xdc322f)); + init_edit_menu(m, "&Edit/"); + let idx = m.add( + "&View/File browser\t", + Shortcut::None, + menu::MenuFlag::Toggle, + cbs::menu_cb, + ); + if load_dir { + m.at(idx).unwrap().set(); + } + #[cfg(feature = "term")] + { + let idx = m.add( + "&View/Terminal\t", + Shortcut::None, + menu::MenuFlag::Toggle, + cbs::menu_cb, + ); + m.at(idx).unwrap().set(); + } + m.add( + "&Help/About\t", + Shortcut::None, + menu::MenuFlag::Normal, + cbs::menu_cb, + ); +} + +pub fn build_editor(id: &str) -> text::TextEditor { + let mut texteditor = text::TextEditor::default().with_id(id); + texteditor.set_color(Color::from_hex(0x002b36)); + texteditor.set_linenumber_width(40); + texteditor.set_linenumber_size(12); + texteditor.set_linenumber_fgcolor(Color::from_hex(0xb58900)); + texteditor.set_linenumber_bgcolor(Color::Background.darker()); + texteditor.set_text_font(Font::Courier); + texteditor.set_trigger(CallbackTrigger::Changed); + texteditor.set_callback(cbs::editor_cb); + texteditor +} + +pub fn create_ed( + tabs: &mut group::Tabs, + id: &str, + current_path: &Option, +) -> text::TextEditor { + tabs.begin(); + let mut edrow = group::Flex::default() + .row() + .with_label(if let Some(current_path) = current_path.as_ref() { + if current_path.is_dir() { + "untitled" + } else { + current_path.file_name().unwrap().to_str().unwrap() + } + } else { + "untitled" + }) + .with_id(id); + edrow.set_trigger(CallbackTrigger::Closed); + edrow.set_callback(cbs::tab_close_cb); + let mut ed = build_editor("ed"); + edrow.end(); + tabs.end(); + tabs.auto_layout(); + tabs.set_value(&edrow).ok(); + + let mut buf = text::TextBuffer::default(); + buf.set_tab_distance(4); + if let Some(p) = current_path.as_ref() { + buf.load_file(p).ok(); + #[cfg(feature = "highlight")] + highlight::highlight(p, &mut ed, &mut buf); + } + ed.set_buffer(buf); + ed +} diff --git a/fltext/src/highlight/colors.rs b/fltext/src/highlight/colors.rs new file mode 100644 index 0000000..35026a2 --- /dev/null +++ b/fltext/src/highlight/colors.rs @@ -0,0 +1,9 @@ +pub const GREEN: u32 = 0x859900; +pub const RED: u32 = 0xdc322f; +pub const YELLOW: u32 = 0xb58900; +pub const DARKYELLOW: u32 = 0xc69a66; +pub const WHITE: u32 = 0xabb2bf; +pub const BLUE: u32 = 0x268bd2; +pub const PURPLE: u32 = 0xc678dd; +pub const GREY: u32 = 0x808080; +pub const LIGHTGREY: u32 = 0xd8d8d8; diff --git a/fltext/src/highlight/md.rs b/fltext/src/highlight/md.rs new file mode 100644 index 0000000..85f09e7 --- /dev/null +++ b/fltext/src/highlight/md.rs @@ -0,0 +1,23 @@ +use super::colors::*; +use super::HighlightData; +use tree_sitter_highlight::HighlightConfiguration; + +use tree_sitter_md as ts; + +pub const STYLES: &[(&str, u32)] = &[ + ("DEFAULT", WHITE), + ("text.title", RED), + ("text.reference", GREY), + ("punctuation.special", RED), + ("text.literal", GREEN), + ("punctuation.delimiter", DARKYELLOW), + ("text.uri", DARKYELLOW), +]; + +pub fn lang_data() -> HighlightData { + let (names, styles) = super::resolve_styles(STYLES); + let mut config = + HighlightConfiguration::new(ts::language(), ts::HIGHLIGHT_QUERY_BLOCK, "", "").unwrap(); + config.configure(&names); + HighlightData::new(styles, config, None) +} diff --git a/fltext/src/highlight/mod.rs b/fltext/src/highlight/mod.rs new file mode 100644 index 0000000..fca4094 --- /dev/null +++ b/fltext/src/highlight/mod.rs @@ -0,0 +1,130 @@ +use fltk::{ + app, + enums::{Color, Font}, + prelude::DisplayExt, + text::{StyleTableEntry, TextBuffer, TextEditor}, +}; +use std::path::Path; +use tree_sitter_highlight::HighlightConfiguration; +use tree_sitter_highlight::HighlightEvent; +use tree_sitter_highlight::Highlighter; + +mod colors; +mod md; +mod rust; +mod toml; + +fn translate_style(idx: usize) -> char { + char::from_u32(65 + idx as u32).unwrap() +} + +fn resolve_styles(v: &[(&'static str, u32)]) -> (Vec<&'static str>, Vec) { + let mut names = Vec::new(); + let mut styles = Vec::new(); + for elem in v { + names.push(elem.0); + styles.push(StyleTableEntry { + color: Color::from_hex(elem.1), + font: Font::Courier, + size: app::font_size(), + }); + } + (names, styles) +} + +pub struct HighlightData { + styles: Vec, + config: HighlightConfiguration, + exeption_fn: Option char>, +} + +impl HighlightData { + pub fn new( + styles: Vec, + config: HighlightConfiguration, + exeption_fn: Option char>, + ) -> Self { + Self { + styles, + config, + exeption_fn, + } + } +} + +fn get_highlight(p: &Path) -> Option { + if let Some(ext) = p.extension() { + match ext.to_str().unwrap() { + "rs" => Some(rust::lang_data()), + "toml" => Some(toml::lang_data()), + "md" => Some(md::lang_data()), + _ => None, + } + } else { + None + } +} + +pub fn highlight(p: &Path, ed: &mut TextEditor, buf: &mut TextBuffer) { + if let Some(HighlightData { + styles, + config, + exeption_fn, + }) = get_highlight(p) + { + let mut highlighter = Highlighter::new(); + let mut sbuf = TextBuffer::default(); + ed.set_highlight_data(sbuf.clone(), styles); + apply( + &mut highlighter, + &config, + &buf.text(), + &mut sbuf, + &exeption_fn, + ); + buf.add_modify_callback({ + let buf = buf.clone(); + move |_, _, _, _, _| { + apply( + &mut highlighter, + &config, + &buf.text(), + &mut sbuf, + &exeption_fn, + ); + } + }); + } +} + +fn apply( + highlighter: &mut Highlighter, + config: &HighlightConfiguration, + s: &str, + sbuf: &mut TextBuffer, + exeption_fn: &Option char>, +) { + let highlights = highlighter + .highlight(config, s.as_bytes(), None, |_| None) + .unwrap(); + + let mut local_buf = "A".repeat(s.len()); + let mut curr = 0; + for event in highlights { + match event.unwrap() { + HighlightEvent::HighlightStart(s) => { + curr = s.0; + } + HighlightEvent::Source { start, end } => { + let c = if let Some(f) = exeption_fn { + f(curr, &s[start..end]) + } else { + translate_style(curr) + }; + local_buf.replace_range(start..end, &c.to_string().repeat(end - start)); + } + HighlightEvent::HighlightEnd => curr = 0, + } + } + sbuf.set_text(&local_buf); +} diff --git a/fltext/src/highlight/rust.rs b/fltext/src/highlight/rust.rs new file mode 100644 index 0000000..4c6e487 --- /dev/null +++ b/fltext/src/highlight/rust.rs @@ -0,0 +1,36 @@ +use super::colors::*; +use super::HighlightData; +use tree_sitter_highlight::HighlightConfiguration; + +use tree_sitter_rust as ts; + +pub const STYLES: &[(&str, u32)] = &[ + ("DEFAULT", WHITE), + ("attribute", RED), + ("constructor", DARKYELLOW), + ("comment", GREY), + ("constant", DARKYELLOW), + ("constant.builtin", DARKYELLOW), + ("function", BLUE), + ("function.method", BLUE), + ("keyword", PURPLE), + ("operator", WHITE), + ("property", RED), + ("punctuation.bracket", DARKYELLOW), + ("punctuation.delimiter", WHITE), + ("string", GREEN), + ("type", YELLOW), + ("type.builtin", YELLOW), + ("variable", RED), + ("variable.builtin", RED), + ("variable.parameter", WHITE), + ("label", WHITE), +]; + +pub fn lang_data() -> HighlightData { + let (names, styles) = super::resolve_styles(STYLES); + let mut config = + HighlightConfiguration::new(ts::language(), ts::HIGHLIGHT_QUERY, "", "").unwrap(); + config.configure(&names); + HighlightData::new(styles, config, None) +} diff --git a/fltext/src/highlight/toml.rs b/fltext/src/highlight/toml.rs new file mode 100644 index 0000000..5212d9a --- /dev/null +++ b/fltext/src/highlight/toml.rs @@ -0,0 +1,24 @@ +use super::colors::*; +use super::HighlightData; +use tree_sitter_highlight::HighlightConfiguration; + +use tree_sitter_toml as ts; + +pub const STYLES: &[(&str, u32)] = &[ + ("DEFAULT", RED), + ("property", RED), + ("comment", GREY), + ("string", GREEN), + ("number", GREEN), + ("operator", LIGHTGREY), + ("punctuation", DARKYELLOW), + ("constant.builtin", DARKYELLOW), +]; + +pub fn lang_data() -> HighlightData { + let (names, styles) = super::resolve_styles(STYLES); + let mut config = + HighlightConfiguration::new(ts::language(), ts::HIGHLIGHT_QUERY, "", "").unwrap(); + config.configure(&names); + HighlightData::new(styles, config, None) +} diff --git a/fltext/src/main.rs b/fltext/src/main.rs new file mode 100644 index 0000000..7fb654f --- /dev/null +++ b/fltext/src/main.rs @@ -0,0 +1,18 @@ +use std::env; + +mod cbs; +mod dialogs; +mod fbr; +mod gui; +mod state; +mod utils; + +#[cfg(feature = "highlight")] +mod highlight; + +fn main() { + let (current_file, current_path) = utils::init_args(env::args()); + let a = gui::init_gui(¤t_file, ¤t_path); + state::init_state(current_file, current_path); + a.run().unwrap(); +} diff --git a/fltext/src/state.rs b/fltext/src/state.rs new file mode 100644 index 0000000..c9d7646 --- /dev/null +++ b/fltext/src/state.rs @@ -0,0 +1,143 @@ +#![allow(dead_code)] + +use crate::gui; +use fltk::{app, group, prelude::*, text, utils::oncelock::Lazy}; +use std::collections::HashMap; +use std::{ + path::PathBuf, + sync::atomic::{AtomicU32, Ordering}, +}; + +static COUNT: AtomicU32 = AtomicU32::new(0); + +#[derive(Clone, Debug)] +pub struct MyBuffer { + pub modified: bool, + pub id: String, + pub buf: text::TextBuffer, + pub current_file: Option, +} + +pub struct State { + pub map: HashMap, + pub current_dir: PathBuf, +} + +impl State { + pub fn new(current_dir: PathBuf) -> Self { + let map = HashMap::default(); + State { map, current_dir } + } + pub fn append(&mut self, current_path: Option) { + let mut tabs: group::Tabs = app::widget_from_id("tabs").unwrap(); + let mut open = false; + let mut edid = 0; + for (k, v) in &self.map { + if v.current_file == current_path { + open = true; + edid = *k; + break; + } + } + if !open { + let old_count = COUNT.load(Ordering::Relaxed); + let id = format!("edrow{}", old_count); + COUNT.store(old_count + 1, Ordering::Relaxed); + let ed = gui::create_ed(&mut tabs, &id, ¤t_path); + let mybuf = MyBuffer { + modified: false, + id, + buf: ed.buffer().unwrap(), + current_file: current_path.map(|p| p.canonicalize().unwrap()), + }; + self.map.insert(ed.as_widget_ptr() as usize, mybuf); + } else { + tabs.set_value( + &text::TextEditor::from_dyn_widget_ptr(edid as *mut _) + .unwrap() + .parent() + .unwrap(), + ) + .ok(); + tabs.set_damage(true); + } + } + pub fn current_id(&self) -> Option { + let tabs: group::Tabs = app::widget_from_id("tabs").unwrap(); + if tabs.children() == 0 { + return None; + } + tabs.value() + .unwrap() + .child(0) + .map(|ed| ed.as_widget_ptr() as usize) + } + pub fn was_modified(&mut self, flag: bool) { + let mut tabs: group::Tabs = app::widget_from_id("tabs").unwrap(); + if tabs.children() == 0 { + return; + } + let mut edrow = tabs.value().unwrap(); + if let Some(c) = edrow.child(0) { + let id = c.as_widget_ptr() as usize; + let mybuf = self.map.get_mut(&id).unwrap(); + mybuf.modified = flag; + if let Some(f) = mybuf.current_file.as_ref() { + if flag { + edrow.set_label(&format!("\t{} *", f.file_name().unwrap().to_str().unwrap())); + } else { + edrow.set_label(&format!("\t{}", f.file_name().unwrap().to_str().unwrap())); + } + tabs.redraw(); + } + } + } + pub fn modified(&self) -> bool { + if let Some(current_id) = self.current_id() { + let mybuf = self.map.get(¤t_id).unwrap(); + mybuf.modified + } else { + false + } + } + pub fn buf(&self) -> Option { + if let Some(current_id) = self.current_id() { + let mybuf = self.map.get(¤t_id).unwrap(); + Some(mybuf.buf.clone()) + } else { + None + } + } + pub fn current_file(&self) -> Option { + if let Some(current_id) = self.current_id() { + let mybuf = self.map.get(¤t_id).unwrap(); + mybuf.current_file.clone() + } else { + None + } + } + pub fn set_current_file(&mut self, path: PathBuf) { + if let Some(current_id) = self.current_id() { + let mybuf = self.map.get_mut(¤t_id).unwrap(); + mybuf.current_file = Some(path) + } + } + pub fn current_editor(&self) -> Option { + let tabs: group::Tabs = app::widget_from_id("tabs").unwrap(); + if tabs.children() == 0 { + return None; + } + tabs.value() + .unwrap() + .child(0) + .map(|c| text::TextEditor::from_dyn_widget(&c).unwrap()) + } +} + +pub static STATE: Lazy> = Lazy::new(app::GlobalState::::get); + +pub fn init_state(current_file: Option, current_path: PathBuf) { + let mut state = State::new(current_path); + state.append(current_file); + app::GlobalState::new(state); +} diff --git a/fltext/src/utils.rs b/fltext/src/utils.rs new file mode 100644 index 0000000..2652355 --- /dev/null +++ b/fltext/src/utils.rs @@ -0,0 +1,63 @@ +use std::{ + env, + path::{Path, PathBuf}, + process::Command, +}; + +pub fn strip_unc_path(p: &Path) -> String { + let p = p.to_str().unwrap(); + if let Some(end) = p.strip_prefix("\\\\?\\") { + end.to_string() + } else { + p.to_string() + } +} + +#[allow(dead_code)] +pub fn has_program(prog: &str) -> bool { + // hacky + match Command::new(prog).arg("--version").output() { + Ok(out) => !out.stdout.is_empty(), + _ => false, + } +} + +pub fn init_args(args: env::Args) -> (Option, PathBuf) { + let args: Vec<_> = args.collect(); + let mut current_file: Option = None; + // fix our working dir + if args.len() > 1 { + let path = PathBuf::from(args[1].clone()); + if path.exists() { + if path.is_dir() { + env::set_current_dir(path.clone()).unwrap(); + } else { + current_file = Some(PathBuf::from(path.file_name().unwrap())); + if let Some(parent) = path.parent() { + if parent.exists() { + env::set_current_dir(parent).unwrap(); + } + } + } + } + path + } else { + env::current_dir().unwrap() + }; + + let current_path = env::current_dir().unwrap().canonicalize().unwrap(); + (current_file, current_path) +} + +#[allow(dead_code)] +pub fn can_use_xterm() -> bool { + if cfg!(not(any(target_os = "macos", target_os = "windows"))) { + if let Ok(var) = env::var("XDG_SESSION_TYPE") { + var == "x11" && has_program("xterm") + } else { + env::var("RED_XTERM").is_ok() + } + } else { + false + } +} diff --git a/framebuffer/Cargo.toml b/framebuffer/Cargo.toml index 73be3c6..a746c1d 100644 --- a/framebuffer/Cargo.toml +++ b/framebuffer/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" +fltk = { version = "^1.4", features = ["use-ninja"] } diff --git a/framebuffer/README.md b/framebuffer/README.md index b44ea57..03cd871 100644 --- a/framebuffer/README.md +++ b/framebuffer/README.md @@ -1,4 +1,4 @@ # framebuffer Demo application showing widget drawing using a framebuffer. -![alt_test](ex.jpg) \ No newline at end of file +![alt_test](assets/framebuffer.gif) diff --git a/framebuffer/ex.jpg b/framebuffer/assets/ex.jpg similarity index 100% rename from framebuffer/ex.jpg rename to framebuffer/assets/ex.jpg diff --git a/framebuffer/framebuffer.gif b/framebuffer/assets/framebuffer.gif similarity index 100% rename from framebuffer/framebuffer.gif rename to framebuffer/assets/framebuffer.gif diff --git a/framebuffer/src/main.rs b/framebuffer/src/main.rs index d35bc43..0ad11ce 100644 --- a/framebuffer/src/main.rs +++ b/framebuffer/src/main.rs @@ -1,14 +1,4 @@ -use fltk::{ - app, - frame, - draw, - prelude::*, - window::Window, -}; -use std::{ - thread, - time::Duration -}; +use fltk::{app, draw, frame, prelude::*, window::Window}; const WIDTH: u32 = 600; const HEIGHT: u32 = 400; @@ -34,7 +24,9 @@ fn main() -> Result<(), Box> { let mut framebuf: Vec = vec![0; (WIDTH * HEIGHT * 4) as usize]; let mut world = World::new(); - unsafe { draw::draw_rgba_nocopy(&mut frame, &framebuf); } + unsafe { + draw::draw_rgba_nocopy(&mut frame, &framebuf); + } while app.wait() { world.update(); diff --git a/glium/Cargo.toml b/glium/Cargo.toml index a68dde2..18eb2d1 100644 --- a/glium/Cargo.toml +++ b/glium/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "glium" +name = "glium_demo" version = "0.1.0" authors = ["Mohammed Alyousef "] edition = "2021" @@ -7,5 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +fltk = { version = "^1.4", features = ["use-ninja", "enable-glwindow"] } glium = { version = "0.29", default-features = false } # no need for glutin -fltk = { version = "1", features = ["enable-glwindow"] } \ No newline at end of file diff --git a/glium/README.md b/glium/README.md index 5f86f28..943e6a7 100644 --- a/glium/README.md +++ b/glium/README.md @@ -3,4 +3,4 @@ Demo application showing usage with the glium crate (an Elegant and safe OpenGL Notice the "enable-glwindow" in the Cargo.toml. -![alt_test](ex.jpg) +![alt_test](assets/glium.gif) diff --git a/glium/ex.jpg b/glium/assets/ex.jpg similarity index 100% rename from glium/ex.jpg rename to glium/assets/ex.jpg diff --git a/glium/glium.gif b/glium/assets/glium.gif similarity index 100% rename from glium/glium.gif rename to glium/assets/glium.gif diff --git a/glium/src/main.rs b/glium/src/main.rs index 7221ba2..f0305dd 100644 --- a/glium/src/main.rs +++ b/glium/src/main.rs @@ -1,33 +1,24 @@ #[macro_use] extern crate glium; +use fltk::{prelude::*, *}; use glium::Surface; -use fltk::{ - prelude::*, - *, -}; -use std::{ - rc::Rc, - cell::RefCell, - os::raw::c_void -}; +use std::{cell::RefCell, os::raw::c_void, rc::Rc}; #[derive(Copy, Clone)] - struct Vertex { - position: [f32; 2], - } - - implement_vertex!(Vertex, position); - +struct Vertex { + position: [f32; 2], +} +implement_vertex!(Vertex, position); fn main() { let app = app::App::default(); let mut win = window::GlWindow::default().with_size(730, 430); - win.make_resizable(true); win.set_mode(enums::Mode::Opengl3); win.end(); + win.make_resizable(true); win.show(); let gl_window = Rc::new(RefCell::new(win.clone())); @@ -37,7 +28,8 @@ fn main() { unsafe impl glium::backend::Backend for Backend { fn swap_buffers(&self) -> Result<(), glium::SwapBuffersError> { - Ok(self.gl_window.borrow_mut().swap_buffers()) + self.gl_window.borrow_mut().swap_buffers(); + Ok(()) } unsafe fn get_proc_address(&self, symbol: &str) -> *const c_void { @@ -45,7 +37,10 @@ fn main() { } fn get_framebuffer_dimensions(&self) -> (u32, u32) { - (self.gl_window.borrow().width() as u32, self.gl_window.borrow().height() as u32) + ( + self.gl_window.borrow().width() as u32, + self.gl_window.borrow().height() as u32, + ) } fn is_current(&self) -> bool { @@ -58,13 +53,20 @@ fn main() { } let context = unsafe { - let backend = Backend { gl_window: gl_window }; + let backend = Backend { gl_window }; glium::backend::Context::new(backend, false, Default::default()) - }.unwrap(); - - let vertex1 = Vertex { position: [-0.5, -0.5] }; - let vertex2 = Vertex { position: [ 0.0, 0.5] }; - let vertex3 = Vertex { position: [ 0.5, -0.25] }; + } + .unwrap(); + + let vertex1 = Vertex { + position: [-0.5, -0.5], + }; + let vertex2 = Vertex { + position: [0.0, 0.5], + }; + let vertex3 = Vertex { + position: [0.5, -0.25], + }; let shape = vec![vertex1, vertex2, vertex3]; let vertex_buffer = glium::VertexBuffer::new(&context, &shape).unwrap(); @@ -86,12 +88,21 @@ fn main() { } "#; - let program = glium::Program::from_source(&context, vertex_shader_src, fragment_shader_src, None).unwrap(); + let program = + glium::Program::from_source(&context, vertex_shader_src, fragment_shader_src, None) + .unwrap(); let mut target = glium::Frame::new(context.clone(), context.get_framebuffer_dimensions()); target.clear_color(0.0, 0.0, 1.0, 1.0); - target.draw(&vertex_buffer, &indices, &program, &glium::uniforms::EmptyUniforms, - &Default::default()).unwrap(); + target + .draw( + &vertex_buffer, + indices, + &program, + &glium::uniforms::EmptyUniforms, + &Default::default(), + ) + .unwrap(); target.finish().unwrap(); app.run().unwrap(); diff --git a/glow/Cargo.toml b/glow/Cargo.toml index 975092e..cf2bbe7 100644 --- a/glow/Cargo.toml +++ b/glow/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "glow" -version = "0.1.0" authors = ["MoAlyousef "] +name = "glow_demo" +version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = { version = "1", features = ["enable-glwindow"] } -glow = "0.7" +fltk = { version = "^1.4", features = ["use-ninja", "enable-glwindow"] } +glow = "^0.13" diff --git a/glow/README.md b/glow/README.md index dc66378..22caca2 100644 --- a/glow/README.md +++ b/glow/README.md @@ -3,4 +3,4 @@ Demo application showing usage with the glow crate (another Opengl bindings crat Notice the "enable-glwindow" in the Cargo.toml. -![alt_test](ex.jpg) +![alt_test](assets/glow.gif) diff --git a/glow/ex.jpg b/glow/assets/ex.jpg similarity index 100% rename from glow/ex.jpg rename to glow/assets/ex.jpg diff --git a/glow/glow.gif b/glow/assets/glow.gif similarity index 100% rename from glow/glow.gif rename to glow/assets/glow.gif diff --git a/glow/src/main.rs b/glow/src/main.rs index 6c22387..e1fc2c5 100644 --- a/glow/src/main.rs +++ b/glow/src/main.rs @@ -1,11 +1,7 @@ -use fltk::{ - prelude::*, - *, -}; +use fltk::{prelude::*, *}; use glow::*; fn main() { - let app = app::App::default(); let mut win = window::GlWindow::default().with_size(800, 600); win.make_resizable(true); @@ -14,9 +10,7 @@ fn main() { win.show(); unsafe { - let gl = glow::Context::from_loader_function(|s| { - win.get_proc_address(s) as *const _ - }); + let gl = glow::Context::from_loader_function(|s| win.get_proc_address(s) as *const _); let vertex_array = gl .create_vertex_array() diff --git a/glut/.gitignore b/glut/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/glut/Cargo.toml b/glut/Cargo.toml index a77f223..cfda53d 100644 --- a/glut/Cargo.toml +++ b/glut/Cargo.toml @@ -1,11 +1,11 @@ [package] +authors = ["Mohammed Alyousef "] name = "glut" version = "0.1.0" -authors = ["Mohammed Alyousef "] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -gl = "0.14" -fltk = { version = "1", features = ["enable-glwindow"] } +fltk = { version = "^1.4", features = ["use-ninja", "enable-glwindow"] } +gl = "^0.14" diff --git a/glut/README.md b/glut/README.md index b4b6aa2..476fab0 100644 --- a/glut/README.md +++ b/glut/README.md @@ -3,4 +3,4 @@ Demo application showing usage with the gl crate (OpenGL function pointer loader Notice the "enable-glwindow" in the Cargo.toml. -![alt_test](ex.png) \ No newline at end of file +![alt_test](assets/glut.gif) diff --git a/glut/ex.png b/glut/assets/ex.png similarity index 100% rename from glut/ex.png rename to glut/assets/ex.png diff --git a/glut/glut.gif b/glut/assets/glut.gif similarity index 100% rename from glut/glut.gif rename to glut/assets/glut.gif diff --git a/glut/src/main.rs b/glut/src/main.rs index 234f288..cb179e9 100644 --- a/glut/src/main.rs +++ b/glut/src/main.rs @@ -1,26 +1,19 @@ -use fltk::{ - prelude::*, - *, -}; - +use fltk::{prelude::*, *}; use gl::types::*; -use std::ffi::CString; -use std::mem; -use std::ptr; -use std::str; +use std::{ffi::CString, mem, ptr, str}; // Vertex data static VERTEX_DATA: [GLfloat; 6] = [0.0, 0.5, 0.5, -0.5, -0.5, -0.5]; // Shader sources -static VS_SRC: &'static str = " +static VS_SRC: &str = " #version 150 in vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); }"; -static FS_SRC: &'static str = " +static FS_SRC: &str = " #version 150 out vec4 out_color; void main() { @@ -44,8 +37,7 @@ fn compile_shader(src: &str, ty: GLenum) -> GLuint { if status != (gl::TRUE as GLint) { let mut len = 0; gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut len); - let mut buf = Vec::with_capacity(len as usize); - buf.set_len((len as usize) - 1); // subtract 1 to skip the trailing null character + let mut buf: Vec = vec![0; (len as usize) - 1]; gl::GetShaderInfoLog( shader, len, @@ -54,9 +46,7 @@ fn compile_shader(src: &str, ty: GLenum) -> GLuint { ); panic!( "{}", - str::from_utf8(&buf) - .ok() - .expect("ShaderInfoLog not valid utf8") + str::from_utf8(&buf).expect("ShaderInfoLog not valid utf8") ); } } @@ -77,8 +67,7 @@ fn link_program(vs: GLuint, fs: GLuint) -> GLuint { if status != (gl::TRUE as GLint) { let mut len: GLint = 0; gl::GetProgramiv(program, gl::INFO_LOG_LENGTH, &mut len); - let mut buf = Vec::with_capacity(len as usize); - buf.set_len((len as usize) - 1); // subtract 1 to skip the trailing null character + let mut buf: Vec = vec![0; (len as usize) - 1]; gl::GetProgramInfoLog( program, len, @@ -87,9 +76,7 @@ fn link_program(vs: GLuint, fs: GLuint) -> GLuint { ); panic!( "{}", - str::from_utf8(&buf) - .ok() - .expect("ProgramInfoLog not valid utf8") + str::from_utf8(&buf).expect("ProgramInfoLog not valid utf8") ); } program @@ -125,7 +112,7 @@ fn main() { gl::BufferData( gl::ARRAY_BUFFER, (VERTEX_DATA.len() * mem::size_of::()) as GLsizeiptr, - mem::transmute(&VERTEX_DATA[0]), + &VERTEX_DATA[0] as *const f32 as *const std::ffi::c_void, gl::STATIC_DRAW, ); diff --git a/glyphmap/Cargo.toml b/glyphmap/Cargo.toml index 28ff21c..b08b1f2 100644 --- a/glyphmap/Cargo.toml +++ b/glyphmap/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1.2" -ttf-parser = "0.14" \ No newline at end of file +fltk = { version = "^1.4", features = ["use-ninja"] } +ttf-parser = "0.14" diff --git a/glyphmap/README.md b/glyphmap/README.md index 4ce0ce1..86d2621 100644 --- a/glyphmap/README.md +++ b/glyphmap/README.md @@ -1,3 +1,5 @@ # glyphmap -A simple glyph/character map made especially to show font icons, which can then be used using `char::from_u32(codepoint);` \ No newline at end of file +A simple glyph/character map made especially to show font icons, which can then be used using `char::from_u32(codepoint);` + +![alt_test](assets/glyphmap.gif) diff --git a/glyphmap/glyphmap.gif b/glyphmap/assets/glyphmap.gif similarity index 100% rename from glyphmap/glyphmap.gif rename to glyphmap/assets/glyphmap.gif diff --git a/glyphmap/image.jpg b/glyphmap/assets/image.jpg similarity index 100% rename from glyphmap/image.jpg rename to glyphmap/assets/image.jpg diff --git a/glyphmap/src/main.rs b/glyphmap/src/main.rs index 3809db1..5d58f46 100644 --- a/glyphmap/src/main.rs +++ b/glyphmap/src/main.rs @@ -1,13 +1,9 @@ +#![forbid(unsafe_code)] extern crate ttf_parser; -use fltk::{ - enums::*, - prelude::*, - *, -}; +use fltk::{enums::*, prelude::*, *}; fn main() { let app = app::App::default(); - app::set_background_color(170, 189, 206); dialog::message_title_default("Glyph Map"); let mut wind = window::Window::default() .with_size(250, 300) @@ -60,8 +56,7 @@ fn main() { let c = char::from_u32(codepoint).unwrap(); let txt = String::from(c); let (w, h) = draw::measure(&txt, true); - if w != 0 && h != 0 { - if face.glyph_index(c).is_some() { + if w != 0 && h != 0 && face.glyph_index(c).is_some() { let hpack = group::Pack::default() .with_type(group::PackType::Horizontal) .with_size(0, 50); @@ -80,7 +75,6 @@ fn main() { out.set_value(&txt); hpack.end(); } - } }); } pack.end(); @@ -94,5 +88,6 @@ fn main() { } }); + app::set_background_color(170, 189, 206); app.run().unwrap(); } diff --git a/gst/Cargo.toml b/gst/Cargo.toml index cc6911d..a342d04 100644 --- a/gst/Cargo.toml +++ b/gst/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = " 1" +fltk = { version = "^1.4", features = ["use-ninja"] } gstreamer = "0.17" -gstreamer-video = "0.17" \ No newline at end of file +gstreamer-video = "0.17" diff --git a/gst/src/main.rs b/gst/src/main.rs index 0a24c21..c39a0d2 100644 --- a/gst/src/main.rs +++ b/gst/src/main.rs @@ -1,8 +1,4 @@ -use fltk::{ - enums::Color, - prelude::*, - *, -}; +use fltk::{enums::Color, prelude::*, *}; use gstreamer_video::prelude::*; #[derive(Copy, Clone)] @@ -12,11 +8,11 @@ pub enum Message { } fn main() { + let app = app::App::default(); gstreamer::init().unwrap(); - let app = app::App::default().with_scheme(app::AppScheme::Gtk); let mut win = window::Window::new(100, 100, 800, 600, "Media Player"); win.make_resizable(true); - + // Create inner window to act as embedded media player let mut gst_win = window::Window::new(10, 10, 780, 520, ""); gst_win.end(); @@ -36,7 +32,7 @@ fn main() { let mut path = String::from("file:///"); let current_dir = std::env::current_dir().unwrap(); let video_file = current_dir.join(uri); - path += &video_file.to_str().unwrap(); + path += video_file.to_str().unwrap(); let playbin = gstreamer::ElementFactory::make("playbin", None).unwrap(); playbin.set_property("uri", &path).unwrap(); @@ -55,16 +51,15 @@ fn main() { but_stop.emit(s, Message::Stop); while app.wait() { - match r.recv() { - Some(val) => match val { + if let Some(val) = r.recv() { + match val { Message::Play => { playbin.set_state(gstreamer::State::Playing).ok(); } Message::Stop => { playbin.set_state(gstreamer::State::Paused).ok(); } - }, - None => (), + } } } } diff --git a/image/Cargo.toml b/image/Cargo.toml index 96257d0..b689a0b 100644 --- a/image/Cargo.toml +++ b/image/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "image" +name = "image_demo" version = "0.1.0" authors = ["MoAlyousef "] edition = "2021" @@ -7,6 +7,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" +fltk = { version = "^1.4", features = ["use-ninja"] } image = "0.24" rust-embed = "6.6.1" diff --git a/image/fltk.png b/image/assets/fltk.png similarity index 100% rename from image/fltk.png rename to image/assets/fltk.png diff --git a/image/src/main.rs b/image/src/main.rs index 306bcdd..4519862 100644 --- a/image/src/main.rs +++ b/image/src/main.rs @@ -1,14 +1,7 @@ +#![forbid(unsafe_code)] #![allow(unused_imports)] -use fltk::{ - app, - draw, - enums::*, - frame, - image as fl_image, - prelude::*, - window, -}; +use fltk::{app, draw, enums::*, frame, image as fl_image, prelude::*, window}; use image::io::Reader as ImageReader; use image::GenericImageView; use std::io::Cursor; @@ -21,24 +14,23 @@ extern crate rust_embed; struct Asset; fn main() { - let img = Asset::get("ex.jpg").unwrap(); - let img = ImageReader::new(Cursor::new(img.data.as_ref())) - .with_guessed_format().unwrap() - .decode().unwrap(); + let app = app::App::default(); + let img = ImageReader::new(Cursor::new(Asset::get("ex.jpg").unwrap().data.as_ref())) + .with_guessed_format() + .unwrap() + .decode() + .unwrap(); let (w, h) = img.dimensions(); - let app = app::App::default(); let mut wind = window::Window::default().with_size(w as i32, h as i32); wind.make_resizable(true); - let mut frame = frame::Frame::default().size_of(&wind); - wind.end(); - wind.show(); - - frame.draw(move |_| { + frame::Frame::default_fill().draw(move |_| { draw::draw_image(&img.to_rgb8(), 0, 0, w as i32, h as i32, ColorDepth::Rgb8).unwrap(); }); + wind.end(); + wind.show(); - // // Or just convert to fltk::image::RgbImage + // Or just convert to fltk::image::RgbImage // let rgb = fl_image::RgbImage::new(&img.to_rgb8(), w, h, ColorDepth::Rgb8).unwrap(); // frame.set_image(Some(rgb)); diff --git a/inner/Cargo.toml b/inner/Cargo.toml new file mode 100644 index 0000000..1daebda --- /dev/null +++ b/inner/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "flrdp" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fltk = { version = "^1.4", features = ["use-ninja"] } diff --git a/inner/README.md b/inner/README.md new file mode 100644 index 0000000..84db8e8 --- /dev/null +++ b/inner/README.md @@ -0,0 +1,3 @@ +# mpv + +Use mpv (the command line app) to play a video inside an fltk window. \ No newline at end of file diff --git a/inner/src/main.rs b/inner/src/main.rs new file mode 100644 index 0000000..26d3874 --- /dev/null +++ b/inner/src/main.rs @@ -0,0 +1,32 @@ +#![forbid(unsafe_code)] +use fltk::{prelude::*, *}; + +fn main() { + let mut window = window::Window::default() + .with_label("flrdp") + .with_size(1280, 720) + .center_screen(); + + let inner = window::Window::new(10, 10, 1260, 700, ""); + inner.end(); + window.end(); + window.make_resizable(true); + window.show(); + + std::process::Command::new("xfreerdp") + .args([ + "+home-drive", + "+clipboard", + "+fonts", + "/cert:ignore", + "/v:127.0.0.1", + &format!("/parent-window:{}", inner.raw_handle()), + ]) + .spawn() + .unwrap(); + + app::App::default() + .with_scheme(app::AppScheme::Plastic) + .run() + .unwrap(); +} diff --git a/libmpv/Cargo.toml b/libmpv/Cargo.toml index d11ba8f..e650f56 100644 --- a/libmpv/Cargo.toml +++ b/libmpv/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "mpv" +name = "libmpv_demo" version = "0.1.0" authors = ["Mohammed Alyousef "] edition = "2021" @@ -7,5 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = {version = "1.1", features=["enable-glwindow", "no-pango"]} +fltk = {version = "^1.4", features=["use-ninja", "enable-glwindow", "no-pango"]} libmpv = { git = "https://github.com/anlumo/libmpv-rs" } diff --git a/libmpv/src/main.rs b/libmpv/src/main.rs index 1075aac..986b592 100644 --- a/libmpv/src/main.rs +++ b/libmpv/src/main.rs @@ -1,15 +1,6 @@ -use fltk::{ - enums::Mode, - prelude::*, - *, -}; +use fltk::{enums::Mode, prelude::*, *}; use libmpv::{ - render::{ - OpenGLInitParams, - RenderContext, - RenderParam, - RenderParamApiType - }, + render::{OpenGLInitParams, RenderContext, RenderParam, RenderParamApiType}, FileState, Mpv, }; use std::os::raw::c_void; @@ -69,7 +60,7 @@ fn main() { w.swap_buffers(); }); - app::add_idle(move || { + app::add_idle3(move |_| { mpv_win.redraw(); }); diff --git a/libvlc/Cargo.toml b/libvlc/Cargo.toml index 77eb046..2ed6fb5 100644 --- a/libvlc/Cargo.toml +++ b/libvlc/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" +fltk = { version = "^1.4", features = ["use-ninja"] } vlc-rs = { git = "https://github.com/garkimasera/vlc-rs.git" } diff --git a/libvlc/README.md b/libvlc/README.md index f1ff626..afd62ab 100644 --- a/libvlc/README.md +++ b/libvlc/README.md @@ -1,6 +1,6 @@ # fltk-vlc Demo application showing embedding a video into an fltk app using the vlc crate -![alt_test](ex.jpg) +![alt_test](assets/ex.jpg) [Tutorial](https://www.youtube.com/watch?v=enxqU3bhCEs) diff --git a/libvlc/ex.jpg b/libvlc/assets/ex.jpg similarity index 100% rename from libvlc/ex.jpg rename to libvlc/assets/ex.jpg diff --git a/libvlc/video.mp4 b/libvlc/assets/video.mp4 similarity index 100% rename from libvlc/video.mp4 rename to libvlc/assets/video.mp4 diff --git a/libvlc/src/main.rs b/libvlc/src/main.rs index 0616781..9f244f0 100644 --- a/libvlc/src/main.rs +++ b/libvlc/src/main.rs @@ -1,8 +1,5 @@ -use fltk::{ - enums::Color, - prelude::*, - *, -}; +#![forbid(unsafe_code)] +use fltk::{enums::Color, prelude::*, *}; use vlc::*; #[derive(Copy, Clone)] @@ -12,30 +9,27 @@ pub enum Message { } fn main() { - let app = app::App::default().with_scheme(app::AppScheme::Gtk); + let app = app::App::default(); + let (sender, receiver) = app::channel::(); let mut win = window::Window::new(100, 100, 800, 600, "Media Player"); win.make_resizable(true); - + // Create inner window to act as embedded media player let mut vlc_win = window::Window::new(10, 10, 780, 520, ""); vlc_win.end(); vlc_win.set_color(Color::Black); - let mut but_play = button::Button::new(320, 545, 80, 40, "Play"); - let mut but_stop = button::Button::new(400, 545, 80, 40, "Stop"); + button::Button::new(320, 545, 80, 40, "Play").emit(sender, Message::Play); + button::Button::new(400, 545, 80, 40, "Stop").emit(sender, Message::Stop); win.end(); win.show(); win.make_resizable(true); - // Take in same args as vlc - let args: Vec = std::env::args().collect(); - // Instantiate vlc instance and media player let instance = Instance::new().unwrap(); - let md = Media::new_path(&instance, "video.mp4").unwrap(); let mdp = MediaPlayer::new(&instance).unwrap(); - mdp.set_media(&md); + mdp.set_media(&Media::new_path(&instance, "video.mp4").unwrap()); // Get vlc_win handle that we'll pass to libvlc // Linux u32, windows HWND, Mac NSWindow @@ -58,18 +52,12 @@ fn main() { mdp.set_key_input(false); mdp.set_mouse_input(false); - let (s, r) = app::channel::(); - - but_play.emit(s, Message::Play); - but_stop.emit(s, Message::Stop); - while app.wait() { - match r.recv() { - Some(val) => match val { + if let Some(val) = receiver.recv() { + match val { Message::Play => mdp.play().unwrap(), Message::Stop => mdp.stop(), - }, - None => (), + } } } } diff --git a/mpv/Cargo.toml b/mpv/Cargo.toml index f791e6c..e4266ec 100644 --- a/mpv/Cargo.toml +++ b/mpv/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" \ No newline at end of file +fltk = { version = "^1.4", features = ["use-ninja"] } diff --git a/mpv/src/main.rs b/mpv/src/main.rs index 5b0238e..2285cc4 100644 --- a/mpv/src/main.rs +++ b/mpv/src/main.rs @@ -1,26 +1,25 @@ -use fltk::{ - enums::Color, - prelude::*, - *, -}; +#[cfg(target_os = "linux")] + +use fltk::{enums::Color, prelude::*, *}; fn main() { - let app = app::App::default().with_scheme(app::AppScheme::Gtk); + let app = app::App::default(); let mut win = window::Window::new(100, 100, 800, 600, "Media Player"); - win.make_resizable(true); - + // Create inner window to act as embedded media player - let mut mpv_win = window::Window::new(10, 10, 780, 520, ""); - mpv_win.end(); - mpv_win.set_color(Color::Black); + let mut inner = window::Window::new(10, 10, 780, 520, ""); + inner.end(); + inner.set_color(Color::Black); win.end(); - win.show(); win.make_resizable(true); + win.show(); - let handle = mpv_win.raw_handle(); std::process::Command::new("mpv") - .args(&[&format!("--wid={}", handle as u64), "../libvlc/video.mp4"]) + .args([ + &format!("--wid={}", inner.raw_handle()), + "../libvlc/video.mp4", + ]) .spawn() .unwrap(); diff --git a/musicplayer/Cargo.toml b/musicplayer/Cargo.toml index 3255478..ed12256 100644 --- a/musicplayer/Cargo.toml +++ b/musicplayer/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1.3.33" -soloud = "1.0.2" +fltk = { version = "^1.4", features = ["use-ninja"] } +soloud = "^1.0" diff --git a/musicplayer/README.md b/musicplayer/README.md index 5a24556..78fe04f 100644 --- a/musicplayer/README.md +++ b/musicplayer/README.md @@ -2,6 +2,6 @@ This example shows using fltk and soloud to create an audio player with customized widgets. -![alt_test](musicplayer.png) +![alt_test](assets/musicplayer.gif) [demo](https://www.youtube.com/watch?v=okdFx6tv7ds) diff --git a/musicplayer/Alarm.mp3 b/musicplayer/assets/Alarm.mp3 similarity index 100% rename from musicplayer/Alarm.mp3 rename to musicplayer/assets/Alarm.mp3 diff --git a/musicplayer/musicplayer.gif b/musicplayer/assets/musicplayer.gif similarity index 100% rename from musicplayer/musicplayer.gif rename to musicplayer/assets/musicplayer.gif diff --git a/musicplayer/musicplayer.png b/musicplayer/assets/musicplayer.png similarity index 100% rename from musicplayer/musicplayer.png rename to musicplayer/assets/musicplayer.png diff --git a/musicplayer/src/fancy_slider.rs b/musicplayer/src/fancy_slider.rs index bdc5a81..aa77814 100644 --- a/musicplayer/src/fancy_slider.rs +++ b/musicplayer/src/fancy_slider.rs @@ -38,4 +38,4 @@ impl DerefMut for FancySlider { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.s } -} \ No newline at end of file +} diff --git a/musicplayer/src/main.rs b/musicplayer/src/main.rs index 9186232..caeb8a5 100644 --- a/musicplayer/src/main.rs +++ b/musicplayer/src/main.rs @@ -1,13 +1,7 @@ -use fltk::{ - app, - enums::*, - frame::*, - prelude::*, - window::*, -}; +#![forbid(unsafe_code)] +use fltk::{app, enums::*, frame::*, prelude::*, window::*}; use soloud::*; -use std::cell::RefCell; -use std::rc::Rc; +use std::{cell::RefCell, rc::Rc}; mod power_button; use power_button::PowerButton; @@ -24,7 +18,7 @@ fn main() { .center_screen() .with_label("Music Player"); wind.make_resizable(true); - + let mut frm = Frame::new(160, 80, 80, 40, TRACK); frm.set_label_size(20); frm.set_label_color(Color::White); @@ -48,7 +42,7 @@ fn main() { return; } let mut wav = audio::Wav::default(); - wav.load(&std::path::Path::new(TRACK)).unwrap(); + wav.load(std::path::Path::new(TRACK)).unwrap(); wav.set_looping(true); sl.borrow().play(&wav); while sl.borrow().active_voice_count() > 0 { diff --git a/musicplayer/src/power_button.rs b/musicplayer/src/power_button.rs index fc9a9d9..3f3fa2e 100644 --- a/musicplayer/src/power_button.rs +++ b/musicplayer/src/power_button.rs @@ -1,9 +1,9 @@ -use fltk::frame::*; -use fltk::image::*; -use fltk::{enums::*, prelude::*}; -use std::cell::RefCell; -use std::ops::{Deref, DerefMut}; -use std::rc::Rc; +use fltk::{enums::*, frame::*, image::*, prelude::*}; +use std::{ + cell::RefCell, + ops::{Deref, DerefMut}, + rc::Rc, +}; const POWER: &str = r#" @@ -50,11 +50,10 @@ const POWER: &str = r#" "#; - #[derive(Clone)] pub struct PowerButton { frm: Frame, - on: Rc> + on: Rc>, } impl PowerButton { @@ -103,4 +102,4 @@ impl DerefMut for PowerButton { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.frm } -} \ No newline at end of file +} diff --git a/opengl/Cargo.toml b/opengl/Cargo.toml index 2943956..c1fc6cb 100644 --- a/opengl/Cargo.toml +++ b/opengl/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = { version = "1", features = ["enable-glwindow"] } +fltk = { version = "^1.4", features = ["use-ninja", "enable-glwindow"] } glu-sys = "0.1.4" diff --git a/opengl/README.md b/opengl/README.md index c804d33..9a6fb89 100644 --- a/opengl/README.md +++ b/opengl/README.md @@ -3,6 +3,6 @@ Demo application showing usage with opengl. Notice the "enable-glwindow" in the Cargo.toml. -![alt_test](ex.jpg) +![alt_test](assets/opengl.gif) -[Demo](https://www.youtube.com/watch?v=5LAR9pSvSfk) \ No newline at end of file +[Demo](https://www.youtube.com/watch?v=5LAR9pSvSfk) diff --git a/opengl/ex.jpg b/opengl/assets/ex.jpg similarity index 100% rename from opengl/ex.jpg rename to opengl/assets/ex.jpg diff --git a/opengl/opengl.gif b/opengl/assets/opengl.gif similarity index 100% rename from opengl/opengl.gif rename to opengl/assets/opengl.gif diff --git a/opengl/src/main.rs b/opengl/src/main.rs index af72936..bcf4b77 100644 --- a/opengl/src/main.rs +++ b/opengl/src/main.rs @@ -1,10 +1,6 @@ +use fltk::{prelude::*, *}; use glu_sys::*; -use fltk::{ - prelude::*, - *, -}; -use std::cell::RefCell; -use std::rc::Rc; +use std::{cell::RefCell, rc::Rc}; const W: i32 = 600; const H: i32 = 400; @@ -32,13 +28,10 @@ pub fn main() { }); while app.wait() { - match r.recv() { - Some(coords) => { - let rand: f32 = ((coords.0 - W / 2) * (coords.1 - H / 2) / 360) as f32; - *rotangle.borrow_mut() += rand; - wind.redraw(); - } - None => (), + if let Some(coords) = r.recv() { + let rand: f32 = ((coords.0 - W / 2) * (coords.1 - H / 2) / 360) as f32; + *rotangle.borrow_mut() += rand; + wind.redraw(); } } } diff --git a/pixels/Cargo.toml b/pixels/Cargo.toml index 9b975ee..55bb08c 100644 --- a/pixels/Cargo.toml +++ b/pixels/Cargo.toml @@ -8,5 +8,5 @@ resolver = "2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = { version = "1", features = ["raw-window-handle"] } +fltk = { version = "1", features = ["use-ninja", "raw-window-handle"] } pixels = "0.9.0" diff --git a/pixels/README.md b/pixels/README.md index 088d9c1..1711101 100644 --- a/pixels/README.md +++ b/pixels/README.md @@ -1,4 +1,4 @@ # Pixels Demo application showing window drawing using a wgpu-accelerated framebuffer (via the pixels crate) -![alt_test](ex.jpg) +![alt_test](assets/pixels.gif) diff --git a/pixels/ex.jpg b/pixels/assets/ex.jpg similarity index 100% rename from pixels/ex.jpg rename to pixels/assets/ex.jpg diff --git a/pixels/pixels.gif b/pixels/assets/pixels.gif similarity index 100% rename from pixels/pixels.gif rename to pixels/assets/pixels.gif diff --git a/plotters/Cargo.toml b/plotters/Cargo.toml index be98153..7f8737c 100644 --- a/plotters/Cargo.toml +++ b/plotters/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "plotters" +name = "plotters_demo" version = "0.1.0" authors = ["Mohammed Alyousef "] edition = "2021" @@ -7,6 +7,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" +fltk = { version = "^1.4", features = ["use-ninja"] } plotters = "0.3.0" plotters-bitmap = "0.3.0" diff --git a/plotters/README.md b/plotters/README.md index 4111407..7ea4e18 100644 --- a/plotters/README.md +++ b/plotters/README.md @@ -1,4 +1,4 @@ # plotters Demo app showing the usage of plotters for live plotting/animations. -![alt_test](ex.jpg) \ No newline at end of file +![alt_test](assets/plotters.gif) diff --git a/plotters/ex.jpg b/plotters/assets/ex.jpg similarity index 100% rename from plotters/ex.jpg rename to plotters/assets/ex.jpg diff --git a/plotters/plotters.gif b/plotters/assets/plotters.gif similarity index 100% rename from plotters/plotters.gif rename to plotters/assets/plotters.gif diff --git a/plotters/src/main.rs b/plotters/src/main.rs index d3ae2e9..549d165 100644 --- a/plotters/src/main.rs +++ b/plotters/src/main.rs @@ -1,20 +1,8 @@ -use fltk::{ - prelude::*, - *, -}; -use plotters::{ - prelude::*, - style::Color, -}; -use plotters_bitmap::{ - BitMapBackend, - bitmap_pixel::RGBPixel -}; -use std::{ - error::Error, - collections::VecDeque, - time::SystemTime -}; +#![forbid(unsafe_code)] +use fltk::{prelude::*, *}; +use plotters::{prelude::*, style::Color}; +use plotters_bitmap::{bitmap_pixel::RGBPixel, BitMapBackend}; +use std::{collections::VecDeque, error::Error, time::SystemTime}; const W: usize = 737; const H: usize = 432; @@ -36,9 +24,8 @@ fn main() -> Result<(), Box> { win.make_resizable(true); win.end(); win.show(); - let root = - BitMapBackend::::with_buffer_and_format(&mut buf, (W as u32, H as u32))? - .into_drawing_area(); + let root = BitMapBackend::::with_buffer_and_format(&mut buf, (W as u32, H as u32))? + .into_drawing_area(); root.fill(&BLACK)?; let mut chart = ChartBuilder::on(&root) @@ -49,7 +36,7 @@ fn main() -> Result<(), Box> { chart .configure_mesh() .label_style(("sans-serif", 15).into_font().color(&GREEN)) - .axis_style(&GREEN) + .axis_style(GREEN) .draw()?; let cs = chart.into_chart_state(); @@ -84,25 +71,23 @@ fn main() -> Result<(), Box> { data.push_back((epoch, phase_x.sin(), phase_y.sin())); if epoch - last_flushed > 1.0 / FREAME_RATE { - let root = BitMapBackend::::with_buffer_and_format( - &mut buf, - (W as u32, H as u32), - )? - .into_drawing_area(); + let root = + BitMapBackend::::with_buffer_and_format(&mut buf, (W as u32, H as u32))? + .into_drawing_area(); let mut chart = cs.clone().restore(&root); chart.plotting_area().fill(&BLACK)?; chart .configure_mesh() - .bold_line_style(&GREEN.mix(0.2)) - .light_line_style(&TRANSPARENT) + .bold_line_style(GREEN.mix(0.2)) + .light_line_style(TRANSPARENT) .draw()?; chart.draw_series(data.iter().zip(data.iter().skip(1)).map( |(&(e, x0, y0), &(_, x1, y1))| { PathElement::new( vec![(x0, y0), (x1, y1)], - &GREEN.mix(((e - epoch) * 20.0).exp()), + GREEN.mix(((e - epoch) * 20.0).exp()), ) }, ))?; diff --git a/raqote/Cargo.toml b/raqote/Cargo.toml index 350c83c..7a22200 100644 --- a/raqote/Cargo.toml +++ b/raqote/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "raqote" +name = "raqote_demo" version = "0.1.0" authors = ["Mohammed Alyousef "] edition = "2021" @@ -7,5 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" -raqote = "0.8" \ No newline at end of file +fltk = { version = "^1.4", features = ["use-ninja"] } +raqote = "0.8" diff --git a/raqote/README.md b/raqote/README.md index ff580a9..c109148 100644 --- a/raqote/README.md +++ b/raqote/README.md @@ -1,4 +1,4 @@ # raqote Demo app showing the usage of raqote for custom drawing on widgets. -![alt_test](ex.jpg) \ No newline at end of file +![alt_test](assets/raqote.gif) diff --git a/raqote/ex.jpg b/raqote/assets/ex.jpg similarity index 100% rename from raqote/ex.jpg rename to raqote/assets/ex.jpg diff --git a/raqote/raqote.gif b/raqote/assets/raqote.gif similarity index 100% rename from raqote/raqote.gif rename to raqote/assets/raqote.gif diff --git a/raqote/src/main.rs b/raqote/src/main.rs index 279a2ea..17e406f 100644 --- a/raqote/src/main.rs +++ b/raqote/src/main.rs @@ -1,19 +1,11 @@ -use raqote::*; use fltk::{ - app, - enums, - frame, - draw, - prelude::{ - WidgetBase, - GroupExt, - WidgetExt - }, + app, draw, enums, frame, + prelude::{GroupExt, WidgetBase, WidgetExt}, window, }; -use std::rc::Rc; +use raqote::*; use std::cell::RefCell; -use fltk::prelude::WindowExt; +use std::rc::Rc; const WIDTH: i32 = 736; const HEIGHT: i32 = 431; @@ -60,7 +52,7 @@ fn main() { y = coords.1; f.redraw(); true - }, + } enums::Event::Drag => { let coords = app::event_coords(); let path = draw_line(x, y, coords.0, coords.1); @@ -86,17 +78,18 @@ fn main() { y = coords.1; f.redraw(); true - }, + } _ => false, }); - unsafe { draw::draw_rgba_nocopy(&mut frame, dt_c.borrow().get_data_u8()); } + unsafe { + draw::draw_rgba_nocopy(&mut frame, dt_c.borrow().get_data_u8()); + } app.run().unwrap(); } pub fn draw_line(x: i32, y: i32, x2: i32, y2: i32) -> Path { let mut pb = PathBuilder::new(); pb.move_to(x as f32, y as f32); - pb.line_to(x2 as f32, y2 as f32); - let path = pb.finish(); - path + pb.line_to(x2 as f32, y2 as f32); + pb.finish() } diff --git a/rounded-svg/Cargo.toml b/rounded-svg/Cargo.toml index 1b24a82..bf97e40 100644 --- a/rounded-svg/Cargo.toml +++ b/rounded-svg/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" -svg = "0.10" \ No newline at end of file +fltk = { version = "^1.4", features = ["use-ninja"] } +svg = "^0.16" diff --git a/rounded-svg/README.md b/rounded-svg/README.md index 72c9572..0d2741a 100644 --- a/rounded-svg/README.md +++ b/rounded-svg/README.md @@ -1,5 +1,6 @@ -# rounded-svg - +# rounded-svg Using the svg crate to create an image with rounded corners. -Translation of: -http://seriss.com/people/erco/fltk/#RoundedCorners \ No newline at end of file + +![alt_test](assets/rounded-svg.gif) + +[Translation of:](http://seriss.com/people/erco/fltk/#RoundedCorners) diff --git a/rounded-svg/rounded-svg.gif b/rounded-svg/assets/rounded-svg.gif similarity index 100% rename from rounded-svg/rounded-svg.gif rename to rounded-svg/assets/rounded-svg.gif diff --git a/rounded-svg/src/main.rs b/rounded-svg/src/main.rs index a803e40..6b6a4ef 100644 --- a/rounded-svg/src/main.rs +++ b/rounded-svg/src/main.rs @@ -1,20 +1,11 @@ -use fltk::{ - enums::*, - prelude::*, - *, -}; +#![forbid(unsafe_code)] +use fltk::{enums::*, prelude::*, *}; use std::{ cell::RefCell, - ops::{ - Deref, - DerefMut - }, + ops::{Deref, DerefMut}, rc::Rc, }; -use svg::{ - node::element::Rectangle, - Document -}; +use svg::{node::element::Rectangle, Document}; struct RoundedImageDisplay { frame_: frame::Frame, diff --git a/speedy2d/Cargo.toml b/speedy2d/Cargo.toml index d41e6b1..ccad48b 100644 --- a/speedy2d/Cargo.toml +++ b/speedy2d/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "speedy" +name = "speedy_demo" version = "0.1.0" authors = ["Mohammed Alyousef "] edition = "2021" @@ -7,6 +7,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = { version = "1", features = ["enable-glwindow"] } +fltk = { version = "^1.4", features = ["use-ninja", "enable-glwindow"] } speedy2d = { version = "1.0.2", default-features = false } gl = "0.14" diff --git a/speedy2d/README.md b/speedy2d/README.md index 3e2a871..9559686 100644 --- a/speedy2d/README.md +++ b/speedy2d/README.md @@ -2,4 +2,4 @@ Uses speedy2D crate to do 2D drawings of a circle and an RGB image in a GlWindow. -![alt_test](ex.jpg) +![alt_test](assets/speedy2d.gif) diff --git a/speedy2d/ex.jpg b/speedy2d/assets/ex.jpg similarity index 100% rename from speedy2d/ex.jpg rename to speedy2d/assets/ex.jpg diff --git a/speedy2d/speedy2d.gif b/speedy2d/assets/speedy2d.gif similarity index 100% rename from speedy2d/speedy2d.gif rename to speedy2d/assets/speedy2d.gif diff --git a/speedy2d/src/main.rs b/speedy2d/src/main.rs index 31702b5..89dcd8a 100644 --- a/speedy2d/src/main.rs +++ b/speedy2d/src/main.rs @@ -1,26 +1,15 @@ use fltk::{ app, enums::Event, - prelude::{ - WidgetBase, - WidgetExt, - GroupExt, - WindowExt - }, - window::{ - GlWindow, - Window - }, - utils + prelude::{GroupExt, WidgetBase, WidgetExt, WindowExt}, + utils, + window::{GlWindow, Window}, }; use speedy2d::{ - GLRenderer, color::Color, dimen::Vector2, - image::{ - ImageDataType, - ImageSmoothingMode - } + image::{ImageDataType, ImageSmoothingMode}, + GLRenderer, }; fn main() { @@ -45,7 +34,7 @@ fn main() { Event::Push => { println!("Pushed"); true - }, + } _ => false, }); @@ -56,13 +45,13 @@ fn main() { renderer.draw_frame(|graphics| { graphics.clear_screen(Color::WHITE); let handle = graphics - .create_image_from_raw_pixels( - ImageDataType::RGB, - ImageSmoothingMode::Linear, - Vector2::new(300, 300), - &fb, - ) - .unwrap(); + .create_image_from_raw_pixels( + ImageDataType::RGB, + ImageSmoothingMode::Linear, + Vector2::new(300, 300), + &fb, + ) + .unwrap(); graphics.draw_image(Vector2::new(0., 0.), &handle); }); diff --git a/systray/Cargo.toml b/systray/Cargo.toml index 731ba8c..2f982d1 100644 --- a/systray/Cargo.toml +++ b/systray/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" +fltk = { version = "^1.4", features = ["use-ninja"] } [target.'cfg(target_os = "windows")'.dependencies] -native-windows-gui = { version = "1.0.10", default-features=false, features=["tray-notification", "message-window", "menu", "cursor"] } \ No newline at end of file +native-windows-gui = { version = "1.0.10", default-features=false, features=["tray-notification", "message-window", "menu", "cursor"] } diff --git a/systray/README.md b/systray/README.md index 027d14a..229f225 100644 --- a/systray/README.md +++ b/systray/README.md @@ -1,3 +1,5 @@ # systray -Demo app showing usage with nwg to create an fltk app with systray functionalities. \ No newline at end of file +Demo app showing usage with nwg to create an fltk app with systray functionalities. + +![systray](assets/systray.gif) diff --git a/systray/sat.ico b/systray/assets/sat.ico similarity index 100% rename from systray/sat.ico rename to systray/assets/sat.ico diff --git a/systray/systray.gif b/systray/assets/systray.gif similarity index 100% rename from systray/systray.gif rename to systray/assets/systray.gif diff --git a/systray/src/main.rs b/systray/src/main.rs index e67915e..27ebd84 100644 --- a/systray/src/main.rs +++ b/systray/src/main.rs @@ -1,18 +1,13 @@ -use fltk::{ - app, - enums::FrameType, - prelude::*, - *, -}; - #[cfg(target_os = "windows")] +use fltk::{app, enums::FrameType, prelude::*, *}; + mod systray; -type HWND = *mut std::os::raw::c_void; -pub static mut WINDOW: HWND = std::ptr::null_mut(); +type Hwnd = *mut std::os::raw::c_void; +pub static mut WINDOW: Hwnd = std::ptr::null_mut(); fn main() { - let app = app::App::default(); + let app = app::App::default().with_scheme(app::Scheme::Plastic); let mut win = window::Window::default().with_size(400, 300); let mut frame = frame::Frame::new(10, 10, 380, 200, ""); frame.set_frame(FrameType::EngravedBox); @@ -23,7 +18,6 @@ fn main() { but.set_callback(move |_| frame.set_label("Hello world!")); - #[cfg(target_os = "windows")] { unsafe { WINDOW = win.raw_handle(); @@ -45,7 +39,5 @@ fn main() { } }); } - - #[cfg(not(target_os = "windows"))] app.run().unwrap(); } diff --git a/systray/src/systray.rs b/systray/src/systray.rs index 44478ad..4aa8567 100644 --- a/systray/src/systray.rs +++ b/systray/src/systray.rs @@ -19,7 +19,7 @@ impl SystemTray { fn show_main_win(&self) { extern "C" { - pub fn ShowWindow(hwnd: crate::HWND, nCmdShow: i32) -> bool; + pub fn ShowWindow(hwnd: crate::Hwnd, nCmdShow: i32) -> bool; } unsafe { ShowWindow(crate::WINDOW, 9); diff --git a/terminal/Cargo.toml b/terminal/Cargo.toml index 314f654..62985a7 100644 --- a/terminal/Cargo.toml +++ b/terminal/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1.3" -portable-pty = "0.7" \ No newline at end of file +fltk = { version = "^1.4", features = ["use-ninja"] } +portable-pty = "0.7" diff --git a/terminal/README.md b/terminal/README.md new file mode 100644 index 0000000..8012a02 --- /dev/null +++ b/terminal/README.md @@ -0,0 +1,3 @@ +# Terminal + +![alt_test](assets/terminal.gif) diff --git a/terminal/terminal.gif b/terminal/assets/terminal.gif similarity index 100% rename from terminal/terminal.gif rename to terminal/assets/terminal.gif diff --git a/terminal/src/main.rs b/terminal/src/main.rs index e888823..0a21760 100644 --- a/terminal/src/main.rs +++ b/terminal/src/main.rs @@ -1,18 +1,8 @@ +#![forbid(unsafe_code)] mod term { - use fltk::{ - enums::*, - prelude::*, - *, - }; - use portable_pty::{ - native_pty_system, - CommandBuilder, - PtySize - }; - use std::io::{ - Read, - Write - }; + use fltk::{enums::*, prelude::*, *}; + use portable_pty::{native_pty_system, CommandBuilder, PtySize}; + use std::io::{Read, Write}; pub struct AnsiTerm { st: text::SimpleTerminal, diff --git a/tinyskia/Cargo.toml b/tinyskia/Cargo.toml index 238fffa..24f2e7e 100644 --- a/tinyskia/Cargo.toml +++ b/tinyskia/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" -tiny-skia = "0.5" \ No newline at end of file +fltk = { version = "^1.4", features = ["use-ninja"] } +tiny-skia = "0.5" diff --git a/tinyskia/README.md b/tinyskia/README.md index b062c0e..e442b54 100644 --- a/tinyskia/README.md +++ b/tinyskia/README.md @@ -1,4 +1,4 @@ # tinyskia Demo app showing the usage of tiny-skia for custom drawing on widgets. -![alt_test](ex.jpg) \ No newline at end of file +![alt_test](assets/tinyskia.gif) diff --git a/tinyskia/ex.jpg b/tinyskia/assets/ex.jpg similarity index 100% rename from tinyskia/ex.jpg rename to tinyskia/assets/ex.jpg diff --git a/tinyskia/tinyskia.gif b/tinyskia/assets/tinyskia.gif similarity index 100% rename from tinyskia/tinyskia.gif rename to tinyskia/assets/tinyskia.gif diff --git a/tinyskia/src/main.rs b/tinyskia/src/main.rs index fb150a8..3318716 100644 --- a/tinyskia/src/main.rs +++ b/tinyskia/src/main.rs @@ -1,21 +1,21 @@ -use fltk::{ - prelude::*, - *, -}; +#![forbid(unsafe_code)] +use fltk::{prelude::*, *}; use tiny_skia::*; fn main() { let triangle = create_triangle(); - let mut paint = Paint::default(); - paint.anti_alias = true; - paint.shader = Pattern::new( - triangle.as_ref(), - SpreadMode::Repeat, - FilterQuality::Bicubic, - 1.0, - Transform::from_row(1.5, -0.4, 0.0, -0.8, 5.0, 1.0), - ); + let paint = Paint { + anti_alias: true, + shader: Pattern::new( + triangle.as_ref(), + SpreadMode::Repeat, + FilterQuality::Bicubic, + 1.0, + Transform::from_row(1.5, -0.4, 0.0, -0.8, 5.0, 1.0), + ), + ..Paint::default() + }; let path = PathBuilder::from_circle(200.0, 200.0, 180.0).unwrap(); diff --git a/web-todo/Cargo.toml b/web-todo/Cargo.toml index cbfed4f..1dba6cf 100644 --- a/web-todo/Cargo.toml +++ b/web-todo/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1.3.33" +fltk = { version = "^1.4", features = ["use-ninja"] } serde = { version = "1.0", features = ["derive"] } reqwest = { version = "0.11", features = ["json"] } tokio = { version = "1.0", features = ["full"] } diff --git a/web-todo/README.md b/web-todo/README.md index 50435ad..aa38c4a 100644 --- a/web-todo/README.md +++ b/web-todo/README.md @@ -1,6 +1,6 @@ # web-todo Client-side todo app using fltk, reqwest, tokio and serde. -![alt_test](ex.jpg) +![alt_test](assets/web-todo.gif) -[Tutorial](https://www.youtube.com/watch?v=tdfFXi4-Yrw) \ No newline at end of file +[Tutorial](https://www.youtube.com/watch?v=tdfFXi4-Yrw) diff --git a/web-todo/ex.jpg b/web-todo/assets/ex.jpg similarity index 100% rename from web-todo/ex.jpg rename to web-todo/assets/ex.jpg diff --git a/web-todo/web-todo.gif b/web-todo/assets/web-todo.gif similarity index 100% rename from web-todo/web-todo.gif rename to web-todo/assets/web-todo.gif diff --git a/web-todo/src/main.rs b/web-todo/src/main.rs index c7f733b..3924fc9 100644 --- a/web-todo/src/main.rs +++ b/web-todo/src/main.rs @@ -1,3 +1,4 @@ +#![forbid(unsafe_code)] use fltk::{enums::*, prelude::*, *}; use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; @@ -74,7 +75,6 @@ async fn main() { win.end(); win.show(); - app::background(255, 255, 255); pack.set_spacing(5); choice.set_color(Color::from_u32(0x673ab7)); choice.set_text_color(Color::White); @@ -115,5 +115,6 @@ async fn main() { }); }); + app::background(255, 255, 255); app.run().unwrap(); } diff --git a/web-todo2/Cargo.toml b/web-todo2/Cargo.toml index 6f5952a..f71d140 100644 --- a/web-todo2/Cargo.toml +++ b/web-todo2/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" +fltk = { version = "^1.4", features = ["use-ninja"] } serde = { version = "1.0", features = ["derive"] } surf = { version = "2.1.0" } async-std = { version = "1.9", features = ["attributes"] } diff --git a/web-todo2/README.md b/web-todo2/README.md index e9d38bf..bc4fb44 100644 --- a/web-todo2/README.md +++ b/web-todo2/README.md @@ -1,4 +1,4 @@ # web-todo2 Client-side todo app using fltk, surf, async-std and serde. -![alt_test](ex.jpg) +![alt_test](assets/web-todo2.gif) diff --git a/web-todo2/ex.jpg b/web-todo2/assets/ex.jpg similarity index 100% rename from web-todo2/ex.jpg rename to web-todo2/assets/ex.jpg diff --git a/web-todo2/web-todo2.gif b/web-todo2/assets/web-todo2.gif similarity index 100% rename from web-todo2/web-todo2.gif rename to web-todo2/assets/web-todo2.gif diff --git a/web-todo2/src/main.rs b/web-todo2/src/main.rs index 3374ac7..c14e410 100644 --- a/web-todo2/src/main.rs +++ b/web-todo2/src/main.rs @@ -1,16 +1,7 @@ -use fltk::{ - enums::*, - prelude::*, - * -}; -use serde::{ - Deserialize, - Serialize -}; -use std::ops::{ - Deref, - DerefMut -}; +#![forbid(unsafe_code)] +use fltk::{enums::*, prelude::*, *}; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; #[derive(Debug, Serialize, Deserialize)] struct Item { diff --git a/webview/Cargo.toml b/webview/Cargo.toml index d1ad21c..1b14dc1 100644 --- a/webview/Cargo.toml +++ b/webview/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "1" -fltk-webview = "0.2" +fltk = { version = "^1.4", features = ["use-ninja"] } +fltk-webview = "^0.3" diff --git a/webview/README.md b/webview/README.md index fe7890a..19b196d 100755 --- a/webview/README.md +++ b/webview/README.md @@ -2,4 +2,4 @@ Works on Windows (MSVC toolchain). -![alt_test](ex.jpg) +![alt_test](assets/webview.gif) diff --git a/webview/ex.jpg b/webview/assets/ex.jpg similarity index 100% rename from webview/ex.jpg rename to webview/assets/ex.jpg diff --git a/webview/webview.gif b/webview/assets/webview.gif similarity index 100% rename from webview/webview.gif rename to webview/assets/webview.gif diff --git a/webview/src/main.rs b/webview/src/main.rs index 1ce1e68..7c819d6 100644 --- a/webview/src/main.rs +++ b/webview/src/main.rs @@ -1,12 +1,8 @@ -use fltk::{ - app, - enums::Event, - prelude::*, - window, -}; +#![forbid(unsafe_code)] +#[cfg(target_os = "linux")] +use fltk::{app, prelude::*, window}; fn main() { - let app = app::App::default(); let mut win = window::Window::default() .with_size(730, 430) .with_label("Webview"); @@ -17,9 +13,9 @@ fn main() { win.end(); win.show(); - let mut wv = fltk_webview::Webview::create(false, &mut wv_win); + let wv = fltk_webview::Webview::create(false, &mut wv_win); wv.navigate("https://google.com"); // the webview handles the main loop - app.run().unwrap(); + app::App::default().run().unwrap(); } diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index feeb056..0bd74b3 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -8,7 +8,7 @@ resolver = "2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = { version = "1", features = ["fltk-bundled", "raw-window-handle"] } +fltk = { version = "1", features = ["use-ninja", "fltk-bundled", "raw-window-handle"] } wgpu = "0.13" pollster = "0.2" diff --git a/wgpu/README.md b/wgpu/README.md index caa1424..06f504f 100644 --- a/wgpu/README.md +++ b/wgpu/README.md @@ -1,2 +1,4 @@ # wgpu -Demo app showing the usage of wgpu to draw on an fltk window. +Demo app showing the usage of wgpu to draw on an fltk window. + +![alt_test](assets/wgpu.gif) diff --git a/wgpu/wgpu.gif b/wgpu/assets/wgpu.gif similarity index 100% rename from wgpu/wgpu.gif rename to wgpu/assets/wgpu.gif diff --git a/xterm/Cargo.toml b/xterm/Cargo.toml index a59d604..6c9a73b 100644 --- a/xterm/Cargo.toml +++ b/xterm/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fltk = "^1.4" +fltk = { version = "^1.4", features = ["use-ninja"] } diff --git a/xterm/src/main.rs b/xterm/src/main.rs index 5f7493d..20f3c68 100644 --- a/xterm/src/main.rs +++ b/xterm/src/main.rs @@ -1,28 +1,35 @@ -use fltk::{ - enums::Color, - prelude::*, - *, -}; +use fltk::{enums::Color, prelude::*, *}; fn main() { - let app = app::App::default().with_scheme(app::AppScheme::Gtk); let mut win = window::Window::new(100, 100, 800, 600, "Terminal"); // Create inner window to act as embedded terminal - let mut xterm_win = window::Window::new(10, 10, 780, 520, ""); - xterm_win.end(); - xterm_win.set_color(Color::Black); + let mut inner = window::Window::new(10, 10, 780, 520, ""); + inner.end(); + inner.set_color(Color::Black); - win.make_resizable(true); win.end(); - win.show(); win.make_resizable(true); + win.show(); - let mut handle = xterm_win.raw_handle(); std::process::Command::new("xterm") - .args(&["-into", &format!("{}", handle), "-bg", "black", "-fg", "white", "-fa", "'Monospace'", "-fs", "10"]) + .args([ + "-into", + &format!("{}", inner.raw_handle()), + "-bg", + "black", + "-fg", + "white", + "-fa", + "'Monospace'", + "-fs", + "10", + ]) .spawn() .unwrap(); - app.run().unwrap(); + app::App::default() + .with_scheme(app::AppScheme::Gtk) + .run() + .unwrap(); }