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](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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