├── .cargo └── config.toml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-AGPLv3 ├── README.md ├── artworks ├── plato-logo.svg ├── plato.png ├── screenshot01.png ├── screenshot02.png ├── screenshot03.png ├── screenshot04.png ├── swipe_sequences.svg ├── thumbnail01.png ├── thumbnail02.png ├── thumbnail03.png ├── thumbnail04.png └── touch_regions.svg ├── build.sh ├── bundle.sh ├── contrib ├── NickelMenu │ └── plato ├── Settings-sample.toml ├── config-sample.sh ├── convert-dictionary.sh ├── grayscale-256.svg ├── hilbert-256.svg ├── mupdf-upgrade │ └── expand_cmap_codes.py ├── nickel.sh ├── plato.sh ├── unicode_test.epub └── wifi-hooks │ ├── wifi-post-up.sh │ └── wifi-pre-down.sh ├── crates ├── core │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── battery │ │ ├── fake.rs │ │ ├── kobo.rs │ │ └── mod.rs │ │ ├── color.rs │ │ ├── context.rs │ │ ├── device.rs │ │ ├── dictionary │ │ ├── dictreader.rs │ │ ├── errors.rs │ │ ├── indexing.rs │ │ ├── mod.rs │ │ └── testdata │ │ │ ├── case_insensitive_dict.dict │ │ │ ├── case_insensitive_dict.index │ │ │ ├── case_sensitive_dict.dict │ │ │ └── case_sensitive_dict.index │ │ ├── document │ │ ├── djvu.rs │ │ ├── djvulibre_sys.rs │ │ ├── epub │ │ │ └── mod.rs │ │ ├── html │ │ │ ├── css.rs │ │ │ ├── dom.rs │ │ │ ├── engine.rs │ │ │ ├── layout.rs │ │ │ ├── mod.rs │ │ │ ├── parse.rs │ │ │ ├── style.rs │ │ │ └── xml.rs │ │ ├── mod.rs │ │ ├── mupdf_sys.rs │ │ └── pdf.rs │ │ ├── font │ │ ├── freetype_sys.rs │ │ ├── harfbuzz_sys.rs │ │ └── mod.rs │ │ ├── framebuffer │ │ ├── image.rs │ │ ├── ion_sys.rs │ │ ├── kobo1.rs │ │ ├── kobo2.rs │ │ ├── linuxfb_sys.rs │ │ ├── mod.rs │ │ ├── mxcfb_sys.rs │ │ ├── sunxi_sys.rs │ │ └── transform.rs │ │ ├── frontlight │ │ ├── mod.rs │ │ ├── natural.rs │ │ ├── premixed.rs │ │ └── standard.rs │ │ ├── geom.rs │ │ ├── gesture.rs │ │ ├── helpers.rs │ │ ├── input.rs │ │ ├── lib.rs │ │ ├── library.rs │ │ ├── lightsensor │ │ ├── kobo.rs │ │ └── mod.rs │ │ ├── metadata.rs │ │ ├── rtc.rs │ │ ├── settings │ │ ├── mod.rs │ │ └── preset.rs │ │ ├── unit.rs │ │ └── view │ │ ├── battery.rs │ │ ├── button.rs │ │ ├── calculator │ │ ├── bottom_bar.rs │ │ ├── code_area.rs │ │ ├── input_bar.rs │ │ └── mod.rs │ │ ├── clock.rs │ │ ├── common.rs │ │ ├── dialog.rs │ │ ├── dictionary │ │ ├── bottom_bar.rs │ │ └── mod.rs │ │ ├── filler.rs │ │ ├── frontlight.rs │ │ ├── home │ │ ├── address_bar.rs │ │ ├── book.rs │ │ ├── bottom_bar.rs │ │ ├── directories_bar.rs │ │ ├── directory.rs │ │ ├── library_label.rs │ │ ├── mod.rs │ │ ├── navigation_bar.rs │ │ └── shelf.rs │ │ ├── icon.rs │ │ ├── image.rs │ │ ├── input_field.rs │ │ ├── intermission.rs │ │ ├── key.rs │ │ ├── keyboard.rs │ │ ├── label.rs │ │ ├── labeled_icon.rs │ │ ├── menu.rs │ │ ├── menu_entry.rs │ │ ├── mod.rs │ │ ├── named_input.rs │ │ ├── notification.rs │ │ ├── page_label.rs │ │ ├── preset.rs │ │ ├── presets_list.rs │ │ ├── reader │ │ ├── bottom_bar.rs │ │ ├── chapter_label.rs │ │ ├── margin_cropper.rs │ │ ├── mod.rs │ │ ├── results_bar.rs │ │ ├── results_label.rs │ │ └── tool_bar.rs │ │ ├── rotation_values │ │ └── mod.rs │ │ ├── rounded_button.rs │ │ ├── search_bar.rs │ │ ├── sketch │ │ └── mod.rs │ │ ├── slider.rs │ │ ├── top_bar.rs │ │ └── touch_events │ │ └── mod.rs ├── emulator │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── fetcher │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── importer │ ├── Cargo.toml │ └── src │ │ └── main.rs └── plato │ ├── Cargo.toml │ └── src │ ├── app.rs │ └── main.rs ├── css ├── annotations.css ├── bookmarks.css ├── dictionary.css ├── epub.css ├── html.css ├── sysinfo.css └── toc.css ├── dist.sh ├── doc ├── ARTICLE_FETCHER.md ├── BUILD.md ├── GUIDE.md ├── HOOKS.md ├── LIBRARY.md ├── MANUAL.md ├── NAVIGATION.md └── TODO.md ├── download.sh ├── fonts ├── Cormorant-Regular.ttf ├── Delius-Regular.ttf ├── LibertinusSerif-Bold.otf ├── LibertinusSerif-BoldItalic.otf ├── LibertinusSerif-Italic.otf ├── LibertinusSerif-Regular.otf ├── NotoSans-Bold.ttf ├── NotoSans-BoldItalic.ttf ├── NotoSans-Italic.ttf ├── NotoSans-Regular.ttf ├── NotoSerif-Bold.ttf ├── NotoSerif-BoldItalic.ttf ├── NotoSerif-Italic.ttf ├── NotoSerif-Regular.ttf ├── Parisienne-Regular.ttf ├── SourceCodeVariable-Italic.otf ├── SourceCodeVariable-Roman.otf └── VarelaRound-Regular.ttf ├── icons ├── align-center.svg ├── align-justify.svg ├── align-left.svg ├── align-right.svg ├── alternate.svg ├── angle-down.svg ├── angle-left-small.svg ├── angle-left.svg ├── angle-right-small.svg ├── angle-right.svg ├── angle-up.svg ├── arrow-down.svg ├── arrow-left.svg ├── arrow-right.svg ├── arrow-up.svg ├── back.svg ├── bullet.svg ├── check_mark-large.svg ├── check_mark-small.svg ├── check_mark.svg ├── close.svg ├── combine.svg ├── contrast.svg ├── cover.svg ├── crop.svg ├── delete-backward.svg ├── delete-forward.svg ├── dodecahedron.svg ├── double_angle-left.svg ├── double_angle-right.svg ├── enclosed_menu.svg ├── font_family.svg ├── font_size.svg ├── frontlight-disabled.svg ├── frontlight.svg ├── gray.svg ├── home.svg ├── line_height.svg ├── margin.svg ├── menu.svg ├── minus.svg ├── move-backward-short.svg ├── move-backward.svg ├── move-forward-short.svg ├── move-forward.svg ├── plug.svg ├── plus.svg ├── redo.svg ├── return.svg ├── search.svg ├── shift.svg ├── toc.svg └── undo.svg ├── install-importer.sh ├── keyboard-layouts ├── english.json └── russian.json ├── mupdf_wrapper ├── build-kobo.sh ├── build.sh └── mupdf_wrapper.c ├── run-emulator.sh ├── scripts ├── essid.sh ├── ip.sh ├── resume.sh ├── suspend.sh ├── usb-disable.sh ├── usb-enable.sh ├── wifi-disable.sh └── wifi-enable.sh ├── service.sh └── thirdparty ├── build.sh ├── bzip2 ├── Makefile-kobo └── build-kobo.sh ├── djvulibre ├── build-kobo.sh └── kobo.patch ├── download.sh ├── freetype2 └── build-kobo.sh ├── gumbo └── build-kobo.sh ├── harfbuzz └── build-kobo.sh ├── jbig2dec └── build-kobo.sh ├── libjpeg └── build-kobo.sh ├── libpng └── build-kobo.sh ├── mupdf ├── build-kobo.sh └── kobo.patch ├── openjpeg └── build-kobo.sh └── zlib └── build-kobo.sh /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.arm-unknown-linux-gnueabihf] 2 | linker = "arm-linux-gnueabihf-gcc" 3 | rustflags = ["-C", "target-feature=+v7,+vfp3,+neon"] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.rs.bk 2 | /target 3 | /extra 4 | /archives 5 | /resources 6 | /libs 7 | /bin 8 | /Settings.toml 9 | /.metadata.json 10 | /.reading-states 11 | /.fat32-epoch 12 | /.thumbnail-previews 13 | /.trash 14 | /css/*-user.css 15 | /hyphenation-patterns 16 | /dictionaries 17 | /dist 18 | /bundle 19 | /plato-*.zip 20 | /thirdparty/*/* 21 | !/thirdparty/*/*kobo* 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/core", 5 | "crates/plato", 6 | "crates/emulator", 7 | "crates/importer", 8 | "crates/fetcher", 9 | ] 10 | 11 | [profile.release-minsized] 12 | inherits = "release" 13 | panic = "abort" 14 | codegen-units = 1 15 | opt-level = "z" 16 | lto = true 17 | strip = true 18 | -------------------------------------------------------------------------------- /LICENSE-AGPLv3: -------------------------------------------------------------------------------- 1 | Plato -- Document reader for the Kobo e-ink devices. 2 | Copyright (C) 2017 Bastien Dejean. 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as 6 | published by the Free Software Foundation, either version 3 of the 7 | License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](artworks/plato-logo.svg) 2 | 3 | *Plato* is a document reader for *Kobo*'s e-readers. 4 | 5 | Documentation: [GUIDE](doc/GUIDE.md), [MANUAL](doc/MANUAL.md) and [BUILD](doc/BUILD.md). 6 | 7 | ## Supported firmwares 8 | 9 | Any 4.*X*.*Y* firmware, with *X* ≥ 6, will do. 10 | 11 | ## Supported devices 12 | 13 | - *Libra Colour*. 14 | - *Clara Colour*. 15 | - *Clara BW*. 16 | - *Elipsa 2E*. 17 | - *Clara 2E*. 18 | - *Libra 2*. 19 | - *Sage*. 20 | - *Elipsa*. 21 | - *Nia*. 22 | - *Libra H₂O*. 23 | - *Forma*. 24 | - *Clara HD*. 25 | - *Aura H₂O Edition 2*. 26 | - *Aura Edition 2*. 27 | - *Aura ONE*. 28 | - *Glo HD*. 29 | - *Aura H₂O*. 30 | - *Aura*. 31 | - *Glo*. 32 | - *Touch C*. 33 | - *Touch B*. 34 | 35 | ## Supported formats 36 | 37 | - PDF, CBZ, FB2, MOBI, XPS and TXT via [MuPDF](https://mupdf.com/index.html). 38 | - ePUB through a built-in renderer. 39 | - DJVU via [DjVuLibre](http://djvu.sourceforge.net/index.html). 40 | 41 | ## Features 42 | 43 | - Crop the margins. 44 | - Continuous fit-to-width zoom mode with line preserving cuts. 45 | - Rotate the screen (portrait ↔ landscape). 46 | - Adjust the contrast. 47 | - Define words using *dictd* dictionaries. 48 | - Annotations, highlights and bookmarks. 49 | - Retrieve articles from online sources through [hooks](doc/HOOKS.md) (an example *wallabag* [article fetcher](doc/ARTICLE_FETCHER.md) is provided). 50 | 51 | [![Tn01](artworks/thumbnail01.png)](artworks/screenshot01.png) [![Tn02](artworks/thumbnail02.png)](artworks/screenshot02.png) [![Tn03](artworks/thumbnail03.png)](artworks/screenshot03.png) [![Tn04](artworks/thumbnail04.png)](artworks/screenshot04.png) 52 | 53 | ## Donations 54 | 55 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=KNAR2VKYRYUV6) 56 | -------------------------------------------------------------------------------- /artworks/plato-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /artworks/plato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/plato/0d8303af63d3cc5496035d29816c6dbba43d0df5/artworks/plato.png -------------------------------------------------------------------------------- /artworks/screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/plato/0d8303af63d3cc5496035d29816c6dbba43d0df5/artworks/screenshot01.png -------------------------------------------------------------------------------- /artworks/screenshot02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/plato/0d8303af63d3cc5496035d29816c6dbba43d0df5/artworks/screenshot02.png -------------------------------------------------------------------------------- /artworks/screenshot03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/plato/0d8303af63d3cc5496035d29816c6dbba43d0df5/artworks/screenshot03.png -------------------------------------------------------------------------------- /artworks/screenshot04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/plato/0d8303af63d3cc5496035d29816c6dbba43d0df5/artworks/screenshot04.png -------------------------------------------------------------------------------- /artworks/swipe_sequences.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /artworks/thumbnail01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/plato/0d8303af63d3cc5496035d29816c6dbba43d0df5/artworks/thumbnail01.png -------------------------------------------------------------------------------- /artworks/thumbnail02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/plato/0d8303af63d3cc5496035d29816c6dbba43d0df5/artworks/thumbnail02.png -------------------------------------------------------------------------------- /artworks/thumbnail03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/plato/0d8303af63d3cc5496035d29816c6dbba43d0df5/artworks/thumbnail03.png -------------------------------------------------------------------------------- /artworks/thumbnail04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/plato/0d8303af63d3cc5496035d29816c6dbba43d0df5/artworks/thumbnail04.png -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | method=${1:-"fast"} 6 | 7 | [ -e libs -a $# -eq 0 ] && method=skip 8 | 9 | case "$method" in 10 | fast) 11 | ./download.sh 'libs/*' 12 | cd libs 13 | 14 | ln -s libz.so.1 libz.so 15 | ln -s libbz2.so.1.0 libbz2.so 16 | 17 | ln -s libpng16.so.16 libpng16.so 18 | ln -s libjpeg.so.9 libjpeg.so 19 | ln -s libopenjp2.so.7 libopenjp2.so 20 | ln -s libjbig2dec.so.0 libjbig2dec.so 21 | 22 | ln -s libfreetype.so.6 libfreetype.so 23 | ln -s libharfbuzz.so.0 libharfbuzz.so 24 | 25 | ln -s libgumbo.so.1 libgumbo.so 26 | ln -s libdjvulibre.so.21 libdjvulibre.so 27 | 28 | cd ../thirdparty 29 | ./download.sh mupdf 30 | cd .. 31 | ;; 32 | 33 | slow) 34 | shift 35 | cd thirdparty 36 | ./download.sh "$@" 37 | ./build.sh "$@" 38 | cd .. 39 | 40 | [ -e libs ] || mkdir libs 41 | 42 | cp thirdparty/zlib/libz.so libs 43 | cp thirdparty/bzip2/libbz2.so libs 44 | 45 | cp thirdparty/libpng/.libs/libpng16.so libs 46 | cp thirdparty/libjpeg/.libs/libjpeg.so libs 47 | cp thirdparty/openjpeg/build/bin/libopenjp2.so libs 48 | cp thirdparty/jbig2dec/.libs/libjbig2dec.so libs 49 | 50 | cp thirdparty/freetype2/objs/.libs/libfreetype.so libs 51 | cp thirdparty/harfbuzz/src/.libs/libharfbuzz.so libs 52 | 53 | cp thirdparty/gumbo/.libs/libgumbo.so libs 54 | cp thirdparty/djvulibre/libdjvu/.libs/libdjvulibre.so libs 55 | cp thirdparty/mupdf/build/release/libmupdf.so libs 56 | ;; 57 | 58 | skip) 59 | ;; 60 | *) 61 | printf "Unknown build method: %s.\n" "$method" 1>&2 62 | exit 1 63 | ;; 64 | esac 65 | 66 | cd mupdf_wrapper 67 | ./build-kobo.sh 68 | cd .. 69 | 70 | cargo build --release --target=arm-unknown-linux-gnueabihf -p plato 71 | -------------------------------------------------------------------------------- /bundle.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | if [ "$#" -lt 1 ] ; then 4 | printf "Usage: %s NICKEL_MENU_ARCHIVE\n" "${0##*/}" >&2 5 | exit 1 6 | fi 7 | 8 | [ -d dist ] || ./dist.sh 9 | [ -d bundle ] && rm -Rf bundle 10 | 11 | NICKEL_MENU_ARCHIVE=$1 12 | 13 | mkdir bundle 14 | cd bundle || exit 1 15 | 16 | if gzip -tq "$NICKEL_MENU_ARCHIVE"; then 17 | ln -s "$NICKEL_MENU_ARCHIVE" KoboRoot.tgz 18 | else 19 | unzip "$NICKEL_MENU_ARCHIVE" KoboRoot.tgz 20 | fi 21 | 22 | tar -xzvf KoboRoot.tgz 23 | rm KoboRoot.tgz 24 | mv mnt/onboard/.adds . 25 | rm -Rf mnt 26 | 27 | mv ../dist .adds/plato 28 | cp ../contrib/NickelMenu/* .adds/nm 29 | 30 | mkdir .kobo 31 | tar -czvf .kobo/KoboRoot.tgz usr 32 | rm -Rf usr 33 | 34 | FIRMWARE_VERSION=$(basename "$FIRMWARE_ARCHIVE" .zip) 35 | FIRMWARE_VERSION=${FIRMWARE_VERSION##*-} 36 | PLATO_VERSION=$(cargo pkgid -p plato | cut -d '#' -f 2) 37 | 38 | zip -r plato-bundle-"$PLATO_VERSION".zip .adds .kobo 39 | rm -Rf .adds .kobo 40 | -------------------------------------------------------------------------------- /contrib/NickelMenu/plato: -------------------------------------------------------------------------------- 1 | menu_item :main :Plato :cmd_spawn :quiet:/mnt/onboard/.adds/plato/plato.sh 2 | -------------------------------------------------------------------------------- /contrib/config-sample.sh: -------------------------------------------------------------------------------- 1 | # Don't set the framebuffer's depth. 2 | # unset PLATO_SET_FRAMEBUFFER_DEPTH 3 | 4 | # Don't convert StarDict dictionaries. 5 | # unset PLATO_CONVERT_DICTIONARIES 6 | 7 | # Disable hyphenation. 8 | # [ -d hyphenation-patterns ] && rm -rf hyphenation-patterns 9 | -------------------------------------------------------------------------------- /contrib/convert-dictionary.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # Converts a StarDict dictionary to the dictd format. 3 | # The first argument must be the path to the IFO file. 4 | 5 | trap 'exit 1' ERR 6 | 7 | base=${1%.*} 8 | bindir=bin/utils 9 | short_name=$(grep '^bookname=' "$1" | cut -d '=' -f 2) 10 | url=$(grep '^website=' "$1" | cut -d '=' -f 2) 11 | 12 | echo "Converting ${short_name} (${1})." 13 | 14 | [ -e "${base}.dict.dz" ] && "$bindir"/dictzip -d "${base}.dict.dz" 15 | 16 | args="${base}.dict" 17 | 18 | [ -e "${base}.syn" ] && args="$args ${base}.syn" 19 | 20 | # shellcheck disable=SC2086 21 | "$bindir"/sdunpack $args < "${base}.idx" > "${base}.txt" 22 | [ "${short_name%% *}" = "Wiktionary" ] && sed -i 's/^\([\[/].*\)/

\1<\/p>/' "${base}.txt" 23 | "$bindir"/dictfmt --quiet --utf8 --index-keep-orig --headword-separator '|' -s "$short_name" -u "$url" -t "$base" < "${base}.txt" 24 | "$bindir"/dictzip "${base}.dict" 25 | 26 | rm "$1" "${base}.idx" "${base}.txt" 27 | [ -e "${base}.syn" ] && rm "${base}.syn" 28 | -------------------------------------------------------------------------------- /contrib/mupdf-upgrade/expand_cmap_codes.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import re 4 | import sys 5 | 6 | for line in sys.stdin: 7 | m = re.search(r'startCharCode = (\d+), endCharCode = (\d+),', line) 8 | if m is not None: 9 | [start, end] = [int(s) for s in m.groups()] 10 | for i in range(start, end+1): 11 | print(i) 12 | -------------------------------------------------------------------------------- /contrib/nickel.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | export LD_LIBRARY_PATH=/usr/local/Kobo 4 | export QT_GSTREAMER_PLAYBIN_AUDIOSINK=alsasink 5 | export QT_GSTREAMER_PLAYBIN_AUDIOSINK_DEVICE_PARAMETER=bluealsa:DEV=00:00:00:00:00:00 6 | 7 | ( 8 | if [ "$PLATFORM" = "freescale" ] || [ "$PLATFORM" = "mx50-ntx" ] || [ "$PLATFORM" = "mx6sl-ntx" ]; then 9 | usleep 400000 10 | fi 11 | /etc/init.d/on-animator.sh 12 | ) & 13 | 14 | # Let Nickel remounts the SD card read only. 15 | [ -e /dev/mmcblk1p1 ] && umount /mnt/sd 16 | 17 | # Nickel wants the WiFi to be down when it starts 18 | ./scripts/wifi-disable.sh 19 | 20 | # Reset PWD to a sane value, outside of onboard, so that USBMS behaves properly 21 | cd / 22 | # And clear up our own stuff from the env while we're there 23 | unset OLDPWD SERIAL_NUMBER FIRMWARE_VERSION MODEL_NUMBER PRODUCT_ID 24 | 25 | /usr/local/Kobo/hindenburg & 26 | LIBC_FATAL_STDERR_=1 /usr/local/Kobo/nickel -platform kobo -skipFontLoad & 27 | [ "$PLATFORM" != "freescale" ] && udevadm trigger & 28 | -------------------------------------------------------------------------------- /contrib/plato.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | WORKDIR=$(dirname "$0") 4 | cd "$WORKDIR" || exit 1 5 | 6 | PLATO_SET_FRAMEBUFFER_DEPTH=1 7 | PLATO_CONVERT_DICTIONARIES=1 8 | 9 | # shellcheck disable=SC1091 10 | [ -e config.sh ] && . config.sh 11 | 12 | # shellcheck disable=SC2046 13 | export $(grep -sE '^(INTERFACE|WIFI_MODULE|DBUS_SESSION_BUS_ADDRESS|NICKEL_HOME|LANG)=' /proc/"$(pidof -s nickel)"/environ) 14 | sync 15 | killall -TERM nickel hindenburg sickel fickel adobehost foxitpdf iink dhcpcd-dbus dhcpcd fmon > /dev/null 2>&1 16 | 17 | 18 | if [ -e /sys/class/leds/LED ] ; then 19 | LEDS_INTERFACE=/sys/class/leds/LED/brightness 20 | STANDARD_LEDS=1 21 | elif [ -e /sys/class/leds/GLED ] ; then 22 | LEDS_INTERFACE=/sys/class/leds/GLED/brightness 23 | STANDARD_LEDS=1 24 | elif [ -e /sys/class/leds/bd71828-green-led ] ; then 25 | LEDS_INTERFACE=/sys/class/leds/bd71828-green-led/brightness 26 | STANDARD_LEDS=1 27 | elif [ -e /sys/devices/platform/ntx_led/lit ] ; then 28 | LEDS_INTERFACE=/sys/devices/platform/ntx_led/lit 29 | STANDARD_LEDS=0 30 | elif [ -e /sys/devices/platform/pmic_light.1/lit ] ; then 31 | LEDS_INTERFACE=/sys/devices/platform/pmic_light.1/lit 32 | STANDARD_LEDS=0 33 | fi 34 | 35 | # Turn off the LEDs 36 | if [ "$STANDARD_LEDS" -eq 1 ] ; then 37 | echo 0 > "$LEDS_INTERFACE" 38 | else 39 | # https://www.tablix.org/~avian/blog/archives/2013/03/blinken_kindle/ 40 | for ch in 3 4 5; do 41 | echo "ch ${ch}" > "$LEDS_INTERFACE" 42 | echo "cur 1" > "$LEDS_INTERFACE" 43 | echo "dc 0" > "$LEDS_INTERFACE" 44 | done 45 | fi 46 | 47 | # Remount the SD card read-write if it's mounted read-only 48 | grep -q ' /mnt/sd .*[ ,]ro[ ,]' /proc/mounts && mount -o remount,rw /mnt/sd 49 | 50 | # Define environment variables used by `scripts/usb-*.sh` 51 | KOBO_TAG=/mnt/onboard/.kobo/version 52 | if [ -e "$KOBO_TAG" ] ; then 53 | SERIAL_NUMBER=$(cut -f 1 -d ',' "$KOBO_TAG") 54 | FIRMWARE_VERSION=$(cut -f 3 -d ',' "$KOBO_TAG") 55 | MODEL_NUMBER=$(cut -f 6 -d ',' "$KOBO_TAG" | sed -e 's/^[0-]*//') 56 | 57 | # This is a combination of the information given in `FBInk/fbink_device_id.c` 58 | # and `calibre/src/calibre/devices/kobo/driver.py`. 59 | case "$MODEL_NUMBER" in 60 | 3[12]0) PRODUCT_ID=0x4163 ;; # Touch A/B, Touch C 61 | 330) PRODUCT_ID=0x4173 ;; # Glo 62 | 340) PRODUCT_ID=0x4183 ;; # Mini 63 | 350) PRODUCT_ID=0x4193 ;; # Aura HD 64 | 360) PRODUCT_ID=0x4203 ;; # Aura 65 | 370) PRODUCT_ID=0x4213 ;; # Aura H₂O 66 | 371) PRODUCT_ID=0x4223 ;; # Glo HD 67 | 372) PRODUCT_ID=0x4224 ;; # Touch 2.0 68 | 373|381) PRODUCT_ID=0x4225 ;; # Aura ONE, Aura ONE Limited Edition 69 | 374) PRODUCT_ID=0x4227 ;; # Aura H₂O Edition 2 70 | 375) PRODUCT_ID=0x4226 ;; # Aura Edition 2 71 | 376) PRODUCT_ID=0x4228 ;; # Clara HD 72 | 377|380) PRODUCT_ID=0x4229 ;; # Forma, Forma 32GB 73 | 384) PRODUCT_ID=0x4232 ;; # Libra H₂O 74 | 382) PRODUCT_ID=0x4230 ;; # Nia 75 | 387) PRODUCT_ID=0x4233 ;; # Elipsa 76 | 383) PRODUCT_ID=0x4231 ;; # Sage 77 | 388) PRODUCT_ID=0x4234 ;; # Libra 2 78 | 386) PRODUCT_ID=0x4235 ;; # Clara 2E 79 | 389) PRODUCT_ID=0x4236 ;; # Elipsa 2E 80 | 390) PRODUCT_ID=0x4237 ;; # Libra Colour 81 | 393) PRODUCT_ID=0x4238 ;; # Clara Colour 82 | 391) PRODUCT_ID=0x4239 ;; # Clara BW 83 | *) PRODUCT_ID=0x6666 ;; 84 | esac 85 | 86 | export SERIAL_NUMBER FIRMWARE_VERSION MODEL_NUMBER PRODUCT_ID 87 | fi 88 | 89 | export LD_LIBRARY_PATH="libs:${LD_LIBRARY_PATH}" 90 | 91 | [ -e info.log ] && [ "$(stat -c '%s' info.log)" -gt $((1<<18)) ] && mv info.log archive.log 92 | 93 | [ "$PLATO_CONVERT_DICTIONARIES" ] && find -L dictionaries -name '*.ifo' -exec ./convert-dictionary.sh {} \; 94 | 95 | if [ "$PLATO_SET_FRAMEBUFFER_DEPTH" ] ; then 96 | case "${PRODUCT}:${MODEL_NUMBER}" in 97 | kraken:*|pixie:*|dragon:*|phoenix:*|dahlia:*|alyssum:*|pika:*|daylight:*|star:375|snow:374) 98 | ORIG_BPP=$(./bin/utils/fbdepth -g) 99 | ;; 100 | *) 101 | unset ORIG_BPP 102 | ;; 103 | esac 104 | fi 105 | 106 | [ "$ORIG_BPP" ] && ./bin/utils/fbdepth -q -d 8 107 | 108 | LIBC_FATAL_STDERR_=1 ./plato >> info.log 2>&1 109 | 110 | [ "$ORIG_BPP" ] && ./bin/utils/fbdepth -q -d "$ORIG_BPP" 111 | 112 | if [ -e /tmp/reboot ] ; then 113 | reboot 114 | elif [ -e /tmp/power_off ] ; then 115 | poweroff -f 116 | else 117 | ./nickel.sh & 118 | fi 119 | -------------------------------------------------------------------------------- /contrib/unicode_test.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/plato/0d8303af63d3cc5496035d29816c6dbba43d0df5/contrib/unicode_test.epub -------------------------------------------------------------------------------- /contrib/wifi-hooks/wifi-post-up.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Dropbear can generate a host key automatically (-R), but the file location is 4 | # configured at build-time and many ARM builds of dropbear have weird locations. 5 | # In order to specify a location, we need to generate the key manually. 6 | if [ ! -e /etc/dropbear/dropbear_ecdsa_host_key ]; then 7 | mkdir -p /etc/dropbear 8 | /mnt/onboard/.adds/bin/dropbearkey -t ecdsa -f /etc/dropbear/dropbear_ecdsa_host_key 9 | fi 10 | 11 | # Add `-n` to skip password check, for initial user creation or password setting. 12 | /mnt/onboard/.adds/bin/dropbear -r /etc/dropbear/dropbear_ecdsa_host_key -p 2233 13 | -------------------------------------------------------------------------------- /contrib/wifi-hooks/wifi-pre-down.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | killall dropbear 4 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Bastien Dejean "] 3 | name = "plato-core" 4 | version = "0.9.44" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["rlib"] 9 | 10 | [dependencies] 11 | bitflags = "2.6.0" 12 | downcast-rs = "1.2.1" 13 | lazy_static = "1.5.0" 14 | libc = "0.2.164" 15 | png = "0.17.14" 16 | regex = "1.11.1" 17 | serde = { version = "1.0.215", features = ["derive"] } 18 | serde_json = "1.0.133" 19 | titlecase = "3.3.0" 20 | unicode-normalization = "0.1.24" 21 | toml = "0.8.19" 22 | zip = "2.2.1" 23 | kl-hyphenate = "0.7.3" 24 | entities = "1.0.1" 25 | paragraph-breaker = "0.4.4" 26 | xi-unicode = "0.3.0" 27 | septem = "1.1.0" 28 | byteorder = "1.5.0" 29 | flate2 = "1.0.35" 30 | levenshtein = "1.0.5" 31 | nix = { version = "0.29.0", features = ["fs", "ioctl"] } 32 | indexmap = { version = "2.6.0", features = ["serde"] } 33 | anyhow = "1.0.93" 34 | thiserror = "2.0.3" 35 | walkdir = "2.5.0" 36 | globset = "0.4.15" 37 | fxhash = "0.2.1" 38 | rand_core = "0.6.4" 39 | rand_xoshiro = "0.6.0" 40 | percent-encoding = "2.3.1" 41 | chrono = { version = "0.4.38", features = ["serde", "clock"], default-features = false } 42 | -------------------------------------------------------------------------------- /crates/core/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | let target = env::var("TARGET").unwrap(); 5 | 6 | // Cross-compiling for Kobo. 7 | if target == "arm-unknown-linux-gnueabihf" { 8 | println!("cargo:rustc-env=PKG_CONFIG_ALLOW_CROSS=1"); 9 | println!("cargo:rustc-link-search=target/mupdf_wrapper/Kobo"); 10 | println!("cargo:rustc-link-search=libs"); 11 | println!("cargo:rustc-link-lib=dylib=stdc++"); 12 | // Handle the Linux and macOS platforms. 13 | } else { 14 | let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); 15 | match target_os.as_ref() { 16 | "linux" => { 17 | println!("cargo:rustc-link-search=target/mupdf_wrapper/Linux"); 18 | println!("cargo:rustc-link-lib=dylib=stdc++"); 19 | }, 20 | "macos" => { 21 | println!("cargo:rustc-link-search=target/mupdf_wrapper/Darwin"); 22 | println!("cargo:rustc-link-lib=dylib=c++"); 23 | }, 24 | _ => panic!("Unsupported platform: {}.", target_os), 25 | } 26 | 27 | println!("cargo:rustc-link-lib=mupdf-third"); 28 | } 29 | 30 | println!("cargo:rustc-link-lib=z"); 31 | println!("cargo:rustc-link-lib=bz2"); 32 | println!("cargo:rustc-link-lib=jpeg"); 33 | println!("cargo:rustc-link-lib=png16"); 34 | println!("cargo:rustc-link-lib=gumbo"); 35 | println!("cargo:rustc-link-lib=openjp2"); 36 | println!("cargo:rustc-link-lib=jbig2dec"); 37 | } 38 | -------------------------------------------------------------------------------- /crates/core/src/battery/fake.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use super::{Battery, Status}; 3 | 4 | pub struct FakeBattery { 5 | capacity: f32, 6 | status: Status, 7 | } 8 | 9 | impl FakeBattery { 10 | pub fn new() -> FakeBattery { 11 | FakeBattery { capacity: 50.0, status: Status::Discharging } 12 | } 13 | } 14 | 15 | impl Battery for FakeBattery { 16 | fn capacity(&mut self) -> Result, Error> { 17 | Ok(vec![self.capacity]) 18 | } 19 | 20 | fn status(&mut self) -> Result, Error> { 21 | Ok(vec![self.status]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/core/src/battery/kobo.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::path::Path; 3 | use std::io::{Read, Seek, SeekFrom}; 4 | use anyhow::{Error, format_err}; 5 | use crate::device::CURRENT_DEVICE; 6 | use super::{Battery, Status}; 7 | 8 | const BATTERY_INTERFACES: [&str; 3] = ["/sys/class/power_supply/bd71827_bat", 9 | "/sys/class/power_supply/mc13892_bat", 10 | "/sys/class/power_supply/battery"]; 11 | const POWER_COVER_INTERFACE: &str = "/sys/class/misc/cilix"; 12 | 13 | const BATTERY_CAPACITY: &str = "capacity"; 14 | const BATTERY_STATUS: &str = "status"; 15 | 16 | const POWER_COVER_CAPACITY: &str = "cilix_bat_capacity"; 17 | const POWER_COVER_STATUS: &str = "charge_status"; 18 | const POWER_COVER_CONNECTED: &str = "cilix_conn"; 19 | 20 | pub struct PowerCover { 21 | capacity: File, 22 | status: File, 23 | connected: File, 24 | } 25 | 26 | // TODO: health, technology, time_to_full_now, time_to_empty_now 27 | pub struct KoboBattery { 28 | capacity: File, 29 | status: File, 30 | power_cover: Option, 31 | } 32 | 33 | impl KoboBattery { 34 | pub fn new() -> Result { 35 | let base = Path::new(BATTERY_INTERFACES.iter() 36 | .find(|bi| Path::new(bi).exists()) 37 | .ok_or_else(|| format_err!("battery path missing"))?); 38 | let capacity = File::open(base.join(BATTERY_CAPACITY))?; 39 | let status = File::open(base.join(BATTERY_STATUS))?; 40 | let power_cover = if CURRENT_DEVICE.has_power_cover() { 41 | let base = Path::new(POWER_COVER_INTERFACE); 42 | let capacity = File::open(base.join(POWER_COVER_CAPACITY))?; 43 | let status = File::open(base.join(POWER_COVER_STATUS))?; 44 | let connected = File::open(base.join(POWER_COVER_CONNECTED))?; 45 | Some(PowerCover { capacity, status, connected }) 46 | } else { 47 | None 48 | }; 49 | Ok(KoboBattery { capacity, status, power_cover }) 50 | } 51 | } 52 | 53 | impl KoboBattery { 54 | fn is_power_cover_connected(&mut self) -> Result { 55 | if let Some(power_cover) = self.power_cover.as_mut() { 56 | let mut buf = String::new(); 57 | power_cover.connected.seek(SeekFrom::Start(0))?; 58 | power_cover.connected.read_to_string(&mut buf)?; 59 | Ok(buf.trim_end().parse::().map_or(false, |v| v == 1)) 60 | } else { 61 | Ok(false) 62 | } 63 | } 64 | } 65 | 66 | impl Battery for KoboBattery { 67 | fn capacity(&mut self) -> Result, Error> { 68 | let mut buf = String::new(); 69 | self.capacity.seek(SeekFrom::Start(0))?; 70 | self.capacity.read_to_string(&mut buf)?; 71 | let capacity = buf.trim_end().parse::() 72 | .unwrap_or(0.0); 73 | if matches!(self.is_power_cover_connected(), Ok(true)) { 74 | let mut buf = String::new(); 75 | self.power_cover.iter_mut().for_each(|power_cover| { 76 | power_cover.capacity.seek(SeekFrom::Start(0)).ok(); 77 | power_cover.capacity.read_to_string(&mut buf).ok(); 78 | }); 79 | let aux_capacity = buf.trim_end().parse::() 80 | .unwrap_or(0.0); 81 | Ok(vec![capacity, aux_capacity]) 82 | } else { 83 | Ok(vec![capacity]) 84 | } 85 | } 86 | 87 | fn status(&mut self) -> Result, Error> { 88 | let mut buf = String::new(); 89 | self.status.seek(SeekFrom::Start(0))?; 90 | self.status.read_to_string(&mut buf)?; 91 | let status = match buf.trim_end() { 92 | "Discharging" => Status::Discharging, 93 | "Charging" => Status::Charging, 94 | "Not charging" | "Full" => Status::Charged, 95 | _ => Status::Unknown, 96 | 97 | }; 98 | if matches!(self.is_power_cover_connected(), Ok(true)) { 99 | let mut buf = String::new(); 100 | self.power_cover.iter_mut().for_each(|power_cover| { 101 | power_cover.status.seek(SeekFrom::Start(0)).ok(); 102 | power_cover.status.read_to_string(&mut buf).ok(); 103 | }); 104 | let aux_status = match buf.trim_end().parse::() { 105 | Ok(0) => Status::Discharging, 106 | Ok(2) => Status::Charging, 107 | Ok(3) => Status::Charged, 108 | _ => Status::Unknown, 109 | }; 110 | Ok(vec![status, aux_status]) 111 | } else { 112 | Ok(vec![status]) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /crates/core/src/battery/mod.rs: -------------------------------------------------------------------------------- 1 | mod kobo; 2 | mod fake; 3 | 4 | use anyhow::Error; 5 | 6 | pub use self::kobo::KoboBattery; 7 | pub use self::fake::FakeBattery; 8 | 9 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 10 | pub enum Status { 11 | Discharging, 12 | Charging, 13 | Charged, 14 | Unknown 15 | // Full, 16 | } 17 | 18 | impl Status { 19 | pub fn is_wired(self) -> bool { 20 | matches!(self, Status::Charging | Status::Charged) 21 | } 22 | } 23 | 24 | pub trait Battery { 25 | fn capacity(&mut self) -> Result, Error>; 26 | fn status(&mut self) -> Result, Error>; 27 | } 28 | -------------------------------------------------------------------------------- /crates/core/src/dictionary/errors.rs: -------------------------------------------------------------------------------- 1 | //! Errors for the Dict dictionary crate. 2 | use std::error; 3 | 4 | /// Error type, representing the errors which can be returned by the libdict library. 5 | /// 6 | /// This enum represents a handful of custom errors and wraps `io:::Error` and 7 | /// `string::FromUtf8Error`. 8 | #[derive(Debug)] 9 | pub enum DictError { 10 | /// Invalid character, e.g. within the index file; the error contains the erroneous character, 11 | /// and optionally line and position. 12 | InvalidCharacter(char, Option, Option), 13 | /// Occurs whenever a line in an index file misses a column. 14 | MissingColumnInIndex(usize), 15 | /// Invalid file format, contains an explanation an optional path to the 16 | /// file with the invalid file format. 17 | InvalidFileFormat(String, Option), 18 | /// This reports a malicious / malformed index file, which requests a buffer which is too large. 19 | MemoryError, 20 | /// This reports words which are not present in the dictionary. 21 | WordNotFound(String), 22 | /// A wrapped io::Error. 23 | IoError(::std::io::Error), 24 | /// A wrapped Utf8Error. 25 | Utf8Error(::std::string::FromUtf8Error), 26 | /// Errors thrown by the flate2 crate - not really descriptive errors, though. 27 | DeflateError(flate2::DecompressError), 28 | } 29 | 30 | impl ::std::fmt::Display for DictError { 31 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 32 | match *self { 33 | DictError::IoError(ref e) => e.fmt(f), 34 | DictError::Utf8Error(ref e) => e.fmt(f), 35 | DictError::DeflateError(ref err) => write!(f, "Error while using \ 36 | the flate2 crate: {:?}", err), 37 | DictError::MemoryError => write!(f, "not enough memory available"), 38 | DictError::WordNotFound(ref word) => write!(f, "Word not found: {}", word), 39 | DictError::InvalidCharacter(ref ch, ref line, ref pos) => { 40 | let mut ret = write!(f, "Invalid character {}", ch); 41 | if let Some(ln) = *line { 42 | ret = write!(f, " on line {}", ln); 43 | } 44 | if let Some(pos) = *pos { 45 | ret = write!(f, " at position {}", pos); 46 | } 47 | ret 48 | }, 49 | DictError::MissingColumnInIndex(ref lnum) => write!(f, "line {}: not \ 50 | enough -separated columns found, expected at least 3", lnum), 51 | DictError::InvalidFileFormat(ref explanation, ref path) => 52 | write!(f, "{}{}", path.clone().unwrap_or_else(String::new), explanation) 53 | } 54 | } 55 | } 56 | 57 | impl error::Error for DictError { 58 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 59 | match *self { 60 | DictError::IoError(ref err) => err.source(), 61 | DictError::Utf8Error(ref err) => err.source(), 62 | _ => None, 63 | } 64 | } 65 | } 66 | 67 | // Allow seamless coercion from::Error. 68 | impl From<::std::io::Error> for DictError { 69 | fn from(err: ::std::io::Error) -> DictError { 70 | DictError::IoError(err) 71 | } 72 | } 73 | 74 | impl From<::std::string::FromUtf8Error> for DictError { 75 | fn from(err: ::std::string::FromUtf8Error) -> DictError { 76 | DictError::Utf8Error(err) 77 | } 78 | } 79 | 80 | impl From for DictError { 81 | fn from(err: flate2::DecompressError) -> DictError { 82 | DictError::DeflateError(err) 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /crates/core/src/dictionary/testdata/case_insensitive_dict.dict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 00-database-dictfmt-1.12.1 4 | 00-database-short 5 | Test Dict 6 | This file was converted from the original database on: 7 | Fri Jun 12 12:16:13 2020 8 | 9 | The original data is available from: 10 | unknown 11 | 12 | The original data was distributed with the notice shown below. No 13 | additional restrictions are claimed. Please redistribute this changed 14 | version under the same conditions and restriction that apply to the 15 | original version. 16 | 17 | foo 18 | definition 19 | Bar 20 | test for case-sensitivity 21 | あいおい 22 | test for non-latin characters 23 | straße 24 | test for non-latin case-sensitivity 25 | unknown 26 | abeforstßあいお 27 | -------------------------------------------------------------------------------- /crates/core/src/dictionary/testdata/case_insensitive_dict.index: -------------------------------------------------------------------------------- 1 | 00-database-allchars B B 2 | 00-database-alphabet I4 U 3 | 00-database-dictfmt-1.12.1 C b 4 | 00-database-info + Fu 5 | 00-database-short d h 6 | 00-database-url Iw I 7 | 00-database-utf8 A B 8 | bar G7 e 9 | foo Gs P 10 | straße IE s 11 | あいおい HZ r 12 | -------------------------------------------------------------------------------- /crates/core/src/dictionary/testdata/case_sensitive_dict.dict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 00-database-dictfmt-1.12.1 5 | 00-database-short 6 | Case Sensitive Test Dict 7 | This file was converted from the original database on: 8 | Fri Jun 12 15:24:14 2020 9 | 10 | The original data is available from: 11 | unknown 12 | 13 | The original data was distributed with the notice shown below. No 14 | additional restrictions are claimed. Please redistribute this changed 15 | version under the same conditions and restriction that apply to the 16 | original version. 17 | 18 | foo 19 | definition 20 | Bar 21 | test for case-sensitivity 22 | あいおい 23 | test for non-latin characters 24 | straße 25 | test for non-latin case-sensitivity 26 | unknown 27 | Baeforstßあいお 28 | -------------------------------------------------------------------------------- /crates/core/src/dictionary/testdata/case_sensitive_dict.index: -------------------------------------------------------------------------------- 1 | 00-database-allchars B B 2 | 00-database-alphabet JI U 3 | 00-database-case-sensitive C B 4 | 00-database-dictfmt-1.12.1 D b 5 | 00-database-info BO Fu 6 | 00-database-short e w 7 | 00-database-url JA I 8 | 00-database-utf8 A B 9 | Bar HL e 10 | foo G8 P 11 | straße IU s 12 | あいおい Hp r 13 | -------------------------------------------------------------------------------- /crates/core/src/framebuffer/ion_sys.rs: -------------------------------------------------------------------------------- 1 | use nix::ioctl_readwrite; 2 | 3 | const MAGIC: u8 = b'I'; 4 | 5 | ioctl_readwrite!(ion_alloc, MAGIC, 0, IonAllocationData); 6 | ioctl_readwrite!(ion_free, MAGIC, 1, IonHandleData); 7 | ioctl_readwrite!(ion_map, MAGIC, 2, IonFdData); 8 | 9 | pub const ION_HEAP_MASK_CARVEOUT: libc::c_uint = 4; 10 | 11 | type IonUserHandle = libc::c_int; 12 | 13 | #[repr(C)] 14 | pub struct IonAllocationData { 15 | pub len: libc::size_t, 16 | pub align: libc::size_t, 17 | pub heap_id_mask: libc::c_uint, 18 | pub flags: libc::c_uint, 19 | pub handle: IonUserHandle, 20 | } 21 | 22 | #[repr(C)] 23 | pub struct IonHandleData { 24 | pub handle: IonUserHandle, 25 | } 26 | 27 | #[repr(C)] 28 | pub struct IonFdData { 29 | pub handle: IonUserHandle, 30 | pub fd: libc::c_int, 31 | } 32 | -------------------------------------------------------------------------------- /crates/core/src/framebuffer/linuxfb_sys.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::mem; 4 | use std::fs::File; 5 | use std::os::unix::io::AsRawFd; 6 | use anyhow::{Error, Context}; 7 | use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; 8 | 9 | ioctl_read_bad!(read_variable_screen_info, FBIOGET_VSCREENINFO, VarScreenInfo); 10 | ioctl_write_ptr_bad!(write_variable_screen_info, FBIOPUT_VSCREENINFO, VarScreenInfo); 11 | ioctl_read_bad!(read_fixed_screen_info, FBIOGET_FSCREENINFO, FixScreenInfo); 12 | 13 | pub const FBIOGET_VSCREENINFO: libc::c_ulong = 0x4600; 14 | pub const FBIOPUT_VSCREENINFO: libc::c_ulong = 0x4601; 15 | pub const FBIOGET_FSCREENINFO: libc::c_ulong = 0x4602; 16 | 17 | #[repr(C)] 18 | #[derive(Clone, Debug)] 19 | pub struct FixScreenInfo { 20 | pub id: [u8; 16], 21 | pub smem_start: usize, 22 | pub smem_len: u32, 23 | pub kind: u32, 24 | pub type_aux: u32, 25 | pub visual: u32, 26 | pub xpanstep: u16, 27 | pub ypanstep: u16, 28 | pub ywrapstep: u16, 29 | pub line_length: u32, 30 | pub mmio_start: usize, 31 | pub mmio_len: u32, 32 | pub accel: u32, 33 | pub capabilities: u16, 34 | pub reserved: [u16; 2], 35 | } 36 | 37 | #[repr(C)] 38 | #[derive(Clone, Debug)] 39 | pub struct VarScreenInfo { 40 | pub xres: u32, 41 | pub yres: u32, 42 | pub xres_virtual: u32, 43 | pub yres_virtual: u32, 44 | pub xoffset: u32, 45 | pub yoffset: u32, 46 | pub bits_per_pixel: u32, 47 | pub grayscale: u32, 48 | pub red: Bitfield, 49 | pub green: Bitfield, 50 | pub blue: Bitfield, 51 | pub transp: Bitfield, 52 | pub nonstd: u32, 53 | pub activate: u32, 54 | pub height: u32, 55 | pub width: u32, 56 | pub accel_flags: u32, 57 | pub pixclock: u32, 58 | pub left_margin: u32, 59 | pub right_margin: u32, 60 | pub upper_margin: u32, 61 | pub lower_margin: u32, 62 | pub hsync_len: u32, 63 | pub vsync_len: u32, 64 | pub sync: u32, 65 | pub vmode: u32, 66 | pub rotate: u32, 67 | pub colorspace: u32, 68 | pub reserved: [u32; 4], 69 | } 70 | 71 | #[repr(C)] 72 | #[derive(Clone, Debug)] 73 | pub struct Bitfield { 74 | pub offset: u32, 75 | pub length: u32, 76 | pub msb_right: u32, 77 | } 78 | 79 | impl Default for Bitfield { 80 | fn default() -> Self { 81 | unsafe { mem::zeroed() } 82 | } 83 | } 84 | 85 | impl Default for VarScreenInfo { 86 | fn default() -> Self { 87 | unsafe { mem::zeroed() } 88 | } 89 | } 90 | 91 | impl Default for FixScreenInfo { 92 | fn default() -> Self { 93 | unsafe { mem::zeroed() } 94 | } 95 | } 96 | 97 | pub fn fix_screen_info(file: &File) -> Result { 98 | let mut info: FixScreenInfo = Default::default(); 99 | let result = unsafe { 100 | read_fixed_screen_info(file.as_raw_fd(), &mut info) 101 | }; 102 | match result { 103 | Err(e) => Err(Error::from(e).context("can't get fixed screen info")), 104 | _ => Ok(info), 105 | } 106 | } 107 | 108 | pub fn var_screen_info(file: &File) -> Result { 109 | let mut info: VarScreenInfo = Default::default(); 110 | let result = unsafe { 111 | read_variable_screen_info(file.as_raw_fd(), &mut info) 112 | }; 113 | match result { 114 | Err(e) => Err(Error::from(e).context("can't get variable screen info")), 115 | _ => Ok(info), 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/core/src/framebuffer/sunxi_sys.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::mem::ManuallyDrop; 4 | use nix::{ioctl_readwrite_bad, ioctl_write_ptr_bad}; 5 | ioctl_readwrite_bad!(send_update, DISP_EINK_UPDATE2, SunxiDispEinkUpdate2); 6 | ioctl_write_ptr_bad!(wait_for_update, DISP_EINK_WAIT_FRAME_SYNC_COMPLETE, SunxiDispEinkWaitFrameSyncComplete); 7 | 8 | pub const DISP_EINK_UPDATE2: libc::c_ulong = 0x0406; 9 | pub const DISP_EINK_WAIT_FRAME_SYNC_COMPLETE: libc::c_ulong = 0x4014; 10 | 11 | pub const LAYER_MODE_BUFFER: libc::c_int = 0; 12 | pub const DISP_FORMAT_8BIT_GRAY: libc::c_int = 0x50; 13 | pub const DISP_GBR_F: libc::c_int = 0x200; 14 | pub const DISP_BF_NORMAL: libc::c_int = 0; 15 | pub const DISP_SCAN_PROGRESSIVE: libc::c_int = 0; 16 | pub const DISP_EOTF_GAMMA22: libc::c_int = 0x004; 17 | 18 | #[repr(C)] 19 | #[derive(Debug)] 20 | pub struct AreaInfo { 21 | pub x_top: libc::c_uint, 22 | pub y_top: libc::c_uint, 23 | pub x_bottom: libc::c_uint, 24 | pub y_bottom: libc::c_uint, 25 | } 26 | 27 | #[repr(C)] 28 | #[derive(Debug)] 29 | pub struct DispRect { 30 | pub x: libc::c_int, 31 | pub y: libc::c_int, 32 | pub width: libc::c_uint, 33 | pub height: libc::c_uint, 34 | } 35 | 36 | #[repr(C)] 37 | pub struct DispRect64 { 38 | pub x: libc::c_longlong, 39 | pub y: libc::c_longlong, 40 | pub width: libc::c_longlong, 41 | pub height: libc::c_longlong, 42 | } 43 | 44 | #[repr(C)] 45 | #[derive(Debug)] 46 | pub struct DispRectsz { 47 | pub width: libc::c_uint, 48 | pub height: libc::c_uint, 49 | } 50 | 51 | #[repr(C)] 52 | pub struct DispLayerConfig2 { 53 | pub info: DispLayerInfo2, 54 | pub enable: bool, 55 | pub channel: libc::c_uint, 56 | pub layer_id: libc::c_uint, 57 | } 58 | 59 | #[repr(C)] 60 | pub struct DispAtwInfo { 61 | pub used: bool, 62 | pub mode: libc::c_int, 63 | pub b_row: libc::c_uint, 64 | pub b_col: libc::c_uint, 65 | pub colf_fd: libc::c_int, 66 | } 67 | 68 | #[repr(C)] 69 | pub struct DispLayerInfo2 { 70 | pub mode: libc::c_int, 71 | pub zorder: libc::c_uchar, 72 | pub alpha_mode: libc::c_uchar, 73 | pub alpha_value: libc::c_uchar, 74 | pub screen_win: DispRect, 75 | pub b_trd_out: bool, 76 | pub out_trd_mode: libc::c_int, 77 | pub color_fb: ColorFb, 78 | pub id: libc::c_uint, 79 | pub atw: DispAtwInfo, 80 | } 81 | 82 | #[repr(C)] 83 | pub struct DispFbInfo2 { 84 | pub fd: libc::c_int, 85 | pub y8_fd: libc::c_int, 86 | pub size: [DispRectsz; 3], 87 | pub align: [libc::c_uint; 3], 88 | pub format: libc::c_int, 89 | pub color_space: libc::c_int, 90 | pub trd_right_fd: libc::c_int, 91 | pub pre_multiply: bool, 92 | pub crop: DispRect64, 93 | pub flags: libc::c_int, 94 | pub scan: libc::c_int, 95 | pub eotf: libc::c_int, 96 | pub depth: libc::c_int, 97 | pub fbd_en: libc::c_uint, 98 | pub metadata_fd: libc::c_int, 99 | pub metadata_size: libc::c_uint, 100 | pub metadata_flag: libc::c_uint, 101 | } 102 | 103 | #[repr(C)] 104 | pub union ColorFb { 105 | pub color: libc::c_uint, 106 | pub fb: ManuallyDrop, 107 | } 108 | 109 | #[repr(C)] 110 | #[derive(Clone, Debug)] 111 | pub struct SunxiDispEinkUpdate2 { 112 | pub area: *const AreaInfo, 113 | pub layer_num: libc::c_ulong, 114 | pub update_mode: libc::c_ulong, 115 | pub lyr_cfg2: *const DispLayerConfig2, 116 | pub frame_id: *mut libc::c_uint, 117 | pub rotate: *const u32, 118 | pub cfa_use: libc::c_ulong, 119 | } 120 | 121 | #[repr(C)] 122 | #[derive(Clone, Debug)] 123 | pub struct SunxiDispEinkWaitFrameSyncComplete { 124 | pub frame_id: u32, 125 | } 126 | 127 | pub const EINK_INIT_MODE:u32 = 0x01; 128 | pub const EINK_DU_MODE:u32 = 0x02; 129 | pub const EINK_GC16_MODE:u32 = 0x04; 130 | pub const EINK_GC4_MODE:u32 = 0x08; 131 | pub const EINK_A2_MODE:u32 = 0x10; 132 | pub const EINK_GL16_MODE:u32 = 0x20; 133 | pub const EINK_GLR16_MODE:u32 = 0x40; 134 | pub const EINK_GLD16_MODE:u32 = 0x80; 135 | pub const EINK_GU16_MODE:u32 = 0x84; 136 | pub const EINK_GCK16_MODE:u32 = 0x90; 137 | pub const EINK_GLK16_MODE:u32 = 0x94; 138 | pub const EINK_CLEAR_MODE:u32 = 0x88; 139 | pub const EINK_GC4L_MODE:u32 = 0x8c; 140 | pub const EINK_GCC16_MODE:u32 = 0xa0; 141 | 142 | pub const EINK_AUTO_MODE:u32 = 0x0000_8000; 143 | pub const EINK_DITHERING_Y1:u32 = 0x0180_0000; 144 | pub const EINK_DITHERING_Y4:u32 = 0x0280_0000; 145 | pub const EINK_DITHERING_SIMPLE:u32 = 0x0480_0000; 146 | pub const EINK_DITHERING_NTX_Y1:u32 = 0x0880_0000; 147 | 148 | pub const EINK_GAMMA_CORRECT:u32 = 0x0020_0000; 149 | pub const EINK_MONOCHROME:u32 = 0x0040_0000; 150 | pub const EINK_NEGATIVE_MODE:u32 = 0x0001_0000; 151 | pub const EINK_REGAL_MODE:u32 = 0x0008_0000; 152 | pub const EINK_NO_MERGE:u32 = 0x8000_0000; 153 | 154 | pub const EINK_PARTIAL_MODE:u32 = 0x0400; 155 | -------------------------------------------------------------------------------- /crates/core/src/framebuffer/transform.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use super::image::Pixmap; 3 | use crate::color::Color; 4 | 5 | pub type ColorTransform = fn(u32, u32, Color) -> Color; 6 | 7 | const DITHER_PITCH: u32 = 128; 8 | 9 | lazy_static! { 10 | // Tileable blue noise matrix. 11 | pub static ref DITHER_G16_DRIFTS: Vec = { 12 | let pixmap = Pixmap::from_png("resources/blue_noise-128.png").unwrap(); 13 | // The gap between two succesive colors in G16 is 17. 14 | // Map {0 .. 255} to {-8 .. 8}. 15 | pixmap.data().iter().map(|&v| { 16 | match v { 17 | 0..=119 => v as i8 / 15 - 8, 18 | 120 => 0, 19 | 121..=255 => ((v - 121) / 15) as i8, 20 | } 21 | }).collect() 22 | }; 23 | 24 | // Tileable blue noise matrix. 25 | pub static ref DITHER_G2_DRIFTS: Vec = { 26 | let pixmap = Pixmap::from_png("resources/blue_noise-128.png").unwrap(); 27 | // Map {0 .. 255} to {-128 .. 127}. 28 | pixmap.data().iter().map(|&v| { 29 | match v { 30 | 0..=127 => -128 + (v as i8), 31 | 128..=255 => (v - 128) as i8, 32 | } 33 | }).collect() 34 | }; 35 | } 36 | 37 | // Ordered dithering. 38 | // The input color is in {0 .. 255}. 39 | // The output color is in G16. 40 | // G16 := {17 * i | i ∈ {0 .. 15}}. 41 | pub fn transform_dither_g16(x: u32, y: u32, color: Color) -> Color { 42 | let gray = color.gray(); 43 | // Get the address of the drift value. 44 | let addr = (x % DITHER_PITCH) + (y % DITHER_PITCH) * DITHER_PITCH; 45 | // Apply the drift to the input color. 46 | let c = (gray as i16 + DITHER_G16_DRIFTS[addr as usize] as i16).clamp(0, 255); 47 | // Compute the distance to the previous color in G16. 48 | let d = c % 17; 49 | // Return the nearest color in G16. 50 | Color::Gray(if d < 9 { 51 | (c - d) as u8 52 | } else { 53 | (c + (17 - d)) as u8 54 | }) 55 | } 56 | 57 | // Ordered dithering. 58 | // The input color is in {0 .. 255}. 59 | // The output color is in {0, 255}. 60 | pub fn transform_dither_g2(x: u32, y: u32, color: Color) -> Color { 61 | let gray = color.gray(); 62 | // Get the address of the drift value. 63 | let addr = (x % DITHER_PITCH) + (y % DITHER_PITCH) * DITHER_PITCH; 64 | // Apply the drift to the input color. 65 | let c = (gray as i16 + DITHER_G2_DRIFTS[addr as usize] as i16).clamp(0, 255); 66 | // Return the nearest color in G2. 67 | Color::Gray(if c < 128 { 68 | 0 69 | } else { 70 | 255 71 | }) 72 | } 73 | 74 | pub fn transform_identity(_x: u32, _y: u32, color: Color) -> Color { 75 | color 76 | } 77 | -------------------------------------------------------------------------------- /crates/core/src/frontlight/mod.rs: -------------------------------------------------------------------------------- 1 | mod standard; 2 | mod natural; 3 | mod premixed; 4 | 5 | use serde::{Serialize, Deserialize}; 6 | pub use self::standard::StandardFrontlight; 7 | pub use self::natural::NaturalFrontlight; 8 | pub use self::premixed::PremixedFrontlight; 9 | use crate::geom::lerp; 10 | 11 | #[derive(Debug, Copy, Clone, Serialize, Deserialize)] 12 | pub struct LightLevels { 13 | pub intensity: f32, 14 | pub warmth: f32, 15 | } 16 | 17 | impl Default for LightLevels { 18 | fn default() -> Self { 19 | LightLevels { 20 | intensity: 0.0, 21 | warmth: 0.0, 22 | } 23 | } 24 | } 25 | 26 | impl LightLevels { 27 | pub fn interpolate(self, other: Self, t: f32) -> Self { 28 | LightLevels { 29 | intensity: lerp(self.intensity, other.intensity, t), 30 | warmth: lerp(self.warmth, other.warmth, t), 31 | } 32 | } 33 | } 34 | 35 | pub trait Frontlight { 36 | // value is a percentage. 37 | fn set_intensity(&mut self, value: f32); 38 | fn set_warmth(&mut self, value: f32); 39 | fn levels(&self) -> LightLevels; 40 | } 41 | 42 | impl Frontlight for LightLevels { 43 | fn set_intensity(&mut self, value: f32) { 44 | self.intensity = value; 45 | } 46 | 47 | fn set_warmth(&mut self, value: f32) { 48 | self.warmth = value; 49 | } 50 | 51 | fn levels(&self) -> LightLevels { 52 | *self 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/core/src/frontlight/premixed.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::io::Write; 3 | use std::fs::File; 4 | use std::fs::OpenOptions; 5 | use anyhow::Error; 6 | use crate::device::CURRENT_DEVICE; 7 | use super::{Frontlight, LightLevels}; 8 | 9 | const FRONTLIGHT_WHITE: &str = "/sys/class/backlight/mxc_msp430.0/brightness"; 10 | 11 | // Forma 12 | const FRONTLIGHT_ORANGE_A: &str = "/sys/class/backlight/tlc5947_bl/color"; 13 | // Libra H₂O, Clara HD, Libra 2, Clara BW, Libra Colour, Clara Colour 14 | const FRONTLIGHT_ORANGE_B: &str = "/sys/class/backlight/lm3630a_led/color"; 15 | // Sage, Libra 2, Clara 2E, Elipsa 2E 16 | const FRONTLIGHT_ORANGE_C: &str = "/sys/class/leds/aw99703-bl_FL1/color"; 17 | 18 | pub struct PremixedFrontlight { 19 | intensity: f32, 20 | warmth: f32, 21 | white: File, 22 | orange: File, 23 | } 24 | 25 | impl PremixedFrontlight { 26 | pub fn new(intensity: f32, warmth: f32) -> Result { 27 | let white = OpenOptions::new().write(true).open(FRONTLIGHT_WHITE)?; 28 | let orange_path = if Path::new(FRONTLIGHT_ORANGE_C).exists() { 29 | FRONTLIGHT_ORANGE_C 30 | } else if Path::new(FRONTLIGHT_ORANGE_B).exists() { 31 | FRONTLIGHT_ORANGE_B 32 | } else { 33 | FRONTLIGHT_ORANGE_A 34 | }; 35 | let orange = OpenOptions::new().write(true).open(orange_path)?; 36 | Ok(PremixedFrontlight { intensity, warmth, white, orange }) 37 | } 38 | } 39 | 40 | impl Frontlight for PremixedFrontlight { 41 | fn set_intensity(&mut self, intensity: f32) { 42 | let white = intensity.round() as i16; 43 | write!(self.white, "{}", white).unwrap(); 44 | self.intensity = intensity; 45 | } 46 | 47 | fn set_warmth(&mut self, warmth: f32) { 48 | let mut orange = (warmth / 10.0).round() as i16; 49 | if CURRENT_DEVICE.mark() != 8 { 50 | orange = 10 - orange; 51 | } 52 | write!(self.orange, "{}", orange).unwrap(); 53 | self.warmth = warmth; 54 | } 55 | 56 | fn levels(&self) -> LightLevels { 57 | LightLevels { 58 | intensity: self.intensity, 59 | warmth: self.warmth, 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/core/src/frontlight/standard.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::fs::OpenOptions; 3 | use std::os::unix::io::AsRawFd; 4 | use nix::ioctl_write_int_bad; 5 | use anyhow::Error; 6 | use super::{Frontlight, LightLevels}; 7 | 8 | ioctl_write_int_bad!(write_frontlight_intensity, 241); 9 | const FRONTLIGHT_INTERFACE: &str = "/dev/ntx_io"; 10 | 11 | pub struct StandardFrontlight { 12 | value: f32, 13 | interface: File, 14 | } 15 | 16 | impl StandardFrontlight { 17 | pub fn new(value: f32) -> Result { 18 | let interface = OpenOptions::new().write(true) 19 | .open(FRONTLIGHT_INTERFACE)?; 20 | Ok(StandardFrontlight { value, interface }) 21 | } 22 | } 23 | 24 | impl Frontlight for StandardFrontlight { 25 | fn set_intensity(&mut self, value: f32) { 26 | let ret = unsafe { 27 | write_frontlight_intensity(self.interface.as_raw_fd(), 28 | value as libc::c_int) 29 | }; 30 | if ret.is_ok() { 31 | self.value = value; 32 | } 33 | } 34 | 35 | fn set_warmth(&mut self, _value: f32) { } 36 | 37 | fn levels(&self) -> LightLevels { 38 | LightLevels { 39 | intensity: self.value, 40 | warmth: 0.0, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] pub mod geom; 2 | 3 | mod unit; 4 | pub mod color; 5 | pub mod device; 6 | pub mod framebuffer; 7 | pub mod frontlight; 8 | pub mod lightsensor; 9 | pub mod battery; 10 | pub mod input; 11 | pub mod helpers; 12 | mod dictionary; 13 | pub mod document; 14 | pub mod library; 15 | pub mod view; 16 | pub mod metadata; 17 | pub mod rtc; 18 | pub mod settings; 19 | pub mod font; 20 | pub mod context; 21 | pub mod gesture; 22 | 23 | pub use anyhow; 24 | pub use fxhash; 25 | pub use chrono; 26 | pub use globset; 27 | pub use walkdir; 28 | pub use rand_core; 29 | pub use rand_xoshiro; 30 | pub use serde; 31 | pub use serde_json; 32 | pub use png; 33 | -------------------------------------------------------------------------------- /crates/core/src/lightsensor/kobo.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Seek, SeekFrom}; 2 | use std::fs::File; 3 | use anyhow::Error; 4 | use super::LightSensor; 5 | 6 | // The Aura ONE uses a Silicon Graphics light sensor, 7 | // the model code is si114x (where x is 5, 6, or 7). 8 | const VISIBLE_PHOTODIODE: &str = "/sys/devices/virtual/input/input3/als_vis_data"; 9 | 10 | pub struct KoboLightSensor(File); 11 | 12 | impl KoboLightSensor { 13 | pub fn new() -> Result { 14 | let file = File::open(VISIBLE_PHOTODIODE)?; 15 | Ok(KoboLightSensor(file)) 16 | } 17 | } 18 | 19 | impl LightSensor for KoboLightSensor { 20 | fn level(&mut self) -> Result { 21 | let mut buf = String::new(); 22 | self.0.seek(SeekFrom::Start(0))?; 23 | self.0.read_to_string(&mut buf)?; 24 | let value = buf.trim_end().parse()?; 25 | Ok(value) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/core/src/lightsensor/mod.rs: -------------------------------------------------------------------------------- 1 | mod kobo; 2 | 3 | use anyhow::Error; 4 | 5 | pub use self::kobo::KoboLightSensor; 6 | 7 | pub trait LightSensor { 8 | fn level(&mut self) -> Result; 9 | } 10 | 11 | impl LightSensor for u16 { 12 | fn level(&mut self) -> Result { 13 | Ok(*self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /crates/core/src/rtc.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use std::fs::File; 3 | use std::path::Path; 4 | use std::os::unix::io::AsRawFd; 5 | use anyhow::Error; 6 | use nix::{ioctl_read, ioctl_write_ptr, ioctl_none}; 7 | use chrono::{Duration, Utc, Datelike, Timelike}; 8 | 9 | ioctl_read!(rtc_read_alarm, b'p', 0x10, RtcWkalrm); 10 | ioctl_write_ptr!(rtc_write_alarm, b'p', 0x0f, RtcWkalrm); 11 | ioctl_none!(rtc_disable_alarm, b'p', 0x02); 12 | 13 | #[repr(C)] 14 | #[derive(Debug, Clone)] 15 | pub struct RtcTime { 16 | tm_sec: libc::c_int, 17 | tm_min: libc::c_int, 18 | tm_hour: libc::c_int, 19 | tm_mday: libc::c_int, 20 | tm_mon: libc::c_int, 21 | tm_year: libc::c_int, 22 | tm_wday: libc::c_int, 23 | tm_yday: libc::c_int, 24 | tm_isdst: libc::c_int, 25 | } 26 | 27 | impl Default for RtcWkalrm { 28 | fn default() -> Self { 29 | unsafe { mem::zeroed() } 30 | } 31 | } 32 | 33 | #[repr(C)] 34 | #[derive(Debug, Clone)] 35 | pub struct RtcWkalrm { 36 | enabled: libc::c_uchar, 37 | pending: libc::c_uchar, 38 | time: RtcTime, 39 | } 40 | 41 | impl RtcTime { 42 | fn year(&self) -> i32 { 43 | 1900 + self.tm_year as i32 44 | } 45 | } 46 | 47 | impl RtcWkalrm { 48 | pub fn enabled(&self) -> bool { 49 | self.enabled == 1 50 | } 51 | 52 | pub fn year(&self) -> i32 { 53 | self.time.year() 54 | } 55 | } 56 | 57 | pub struct Rtc(File); 58 | 59 | impl Rtc { 60 | pub fn new>(path: P) -> Result { 61 | let file = File::open(path)?; 62 | Ok(Rtc(file)) 63 | } 64 | 65 | pub fn alarm(&self) -> Result { 66 | let mut rwa = RtcWkalrm::default(); 67 | unsafe { 68 | rtc_read_alarm(self.0.as_raw_fd(), &mut rwa) 69 | .map(|_| rwa) 70 | .map_err(|e| e.into()) 71 | } 72 | } 73 | 74 | pub fn set_alarm(&self, days: f32) -> Result { 75 | let wt = Utc::now() + Duration::seconds((86_400.0 * days) as i64); 76 | let rwa = RtcWkalrm { 77 | enabled: 1, 78 | pending: 0, 79 | time: RtcTime { 80 | tm_sec: wt.second() as libc::c_int, 81 | tm_min: wt.minute() as libc::c_int, 82 | tm_hour: wt.hour() as libc::c_int, 83 | tm_mday: wt.day() as libc::c_int, 84 | tm_mon: wt.month0() as libc::c_int, 85 | tm_year: (wt.year() - 1900) as libc::c_int, 86 | tm_wday: -1, 87 | tm_yday: -1, 88 | tm_isdst: -1, 89 | }, 90 | }; 91 | unsafe { rtc_write_alarm(self.0.as_raw_fd(), &rwa).map_err(|e| e.into()) } 92 | } 93 | 94 | pub fn disable_alarm(&self) -> Result { 95 | unsafe { rtc_disable_alarm(self.0.as_raw_fd()).map_err(|e| e.into()) } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/core/src/settings/preset.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Local, Timelike}; 2 | use serde::{Serialize, Deserialize}; 3 | use crate::frontlight::LightLevels; 4 | use crate::geom::circular_distances; 5 | 6 | const MINUTES_PER_DAY: u16 = 24 * 60; 7 | 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | #[serde(default, rename_all = "camelCase")] 10 | pub struct LightPreset { 11 | pub timestamp: u16, 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub lightsensor_level: Option, 14 | pub frontlight_levels: LightLevels, 15 | } 16 | 17 | impl Default for LightPreset { 18 | fn default() -> Self { 19 | let now = Local::now(); 20 | LightPreset { 21 | timestamp: (60 * now.hour() + now.minute()) as u16, 22 | frontlight_levels: LightLevels::default(), 23 | lightsensor_level: None, 24 | } 25 | } 26 | } 27 | 28 | impl LightPreset { 29 | pub fn name(&self) -> String { 30 | let hours = self.timestamp / 60; 31 | let minutes = self.timestamp - hours * 60; 32 | format!("{:02}:{:02}", hours, minutes) 33 | } 34 | } 35 | 36 | pub fn guess_frontlight(lightsensor_level: Option, light_presets: &[LightPreset]) -> Option { 37 | if light_presets.len() < 2 { 38 | return None; 39 | } 40 | let cur = LightPreset { 41 | lightsensor_level, 42 | .. Default::default() 43 | }; 44 | 45 | let mut dmin = [u16::MAX; 2]; 46 | let mut index = [usize::MAX; 2]; 47 | 48 | if light_presets[0].lightsensor_level.is_some() { 49 | let s = cur.lightsensor_level.unwrap_or_default(); 50 | 51 | for (i, lp) in light_presets.iter().enumerate() { 52 | let p = lp.lightsensor_level.unwrap_or_default(); 53 | let d = if s >= p { s - p } else { p - s }; 54 | 55 | if p >= s && d < dmin[0] { 56 | dmin[0] = d; 57 | index[0] = i; 58 | } 59 | 60 | if p <= s && d < dmin[1] { 61 | dmin[1] = d; 62 | index[1] = i; 63 | } 64 | } 65 | } else { 66 | for (i, lp) in light_presets.iter().enumerate() { 67 | let (d0, d1) = circular_distances(cur.timestamp, lp.timestamp, MINUTES_PER_DAY); 68 | 69 | if d0 < dmin[0] { 70 | dmin[0] = d0; 71 | index[0] = i; 72 | } 73 | 74 | if d1 < dmin[1] { 75 | dmin[1] = d1; 76 | index[1] = i; 77 | } 78 | } 79 | } 80 | 81 | if dmin[0] == 0 || dmin[1] == u16::MAX { 82 | return Some(light_presets[index[0]].frontlight_levels); 83 | } 84 | 85 | if dmin[1] == 0 || dmin[0] == u16::MAX { 86 | return Some(light_presets[index[1]].frontlight_levels); 87 | } 88 | 89 | let fl0 = light_presets[index[0]].frontlight_levels; 90 | let fl1 = light_presets[index[1]].frontlight_levels; 91 | let t = dmin[0] as f32 / (dmin[0] + dmin[1]) as f32; 92 | 93 | Some(fl0.interpolate(fl1, t)) 94 | } 95 | -------------------------------------------------------------------------------- /crates/core/src/unit.rs: -------------------------------------------------------------------------------- 1 | pub const MILLIMETERS_PER_INCH: f32 = 25.4; 2 | pub const CENTIMETERS_PER_INCH: f32 = 2.54; 3 | pub const POINTS_PER_INCH: f32 = 72.0; 4 | pub const PICAS_PER_INCH: f32 = 6.0; 5 | const BASE_DPI: f32 = 300.0; 6 | 7 | #[inline] 8 | pub fn pt_to_px(pt: f32, dpi: u16) -> f32 { 9 | pt * (dpi as f32 / POINTS_PER_INCH) 10 | } 11 | 12 | #[inline] 13 | pub fn pc_to_px(pc: f32, dpi: u16) -> f32 { 14 | pc * (dpi as f32 / PICAS_PER_INCH) 15 | } 16 | 17 | #[inline] 18 | pub fn in_to_px(inc: f32, dpi: u16) -> f32 { 19 | inc * (dpi as f32) 20 | } 21 | 22 | #[inline] 23 | pub fn mm_to_px(mm: f32, dpi: u16) -> f32 { 24 | mm * (dpi as f32 / MILLIMETERS_PER_INCH) 25 | } 26 | 27 | #[inline] 28 | pub fn scale_by_dpi_raw(x: f32, dpi: u16) -> f32 { 29 | x * (dpi as f32) / BASE_DPI 30 | } 31 | 32 | #[inline] 33 | pub fn scale_by_dpi(x: f32, dpi: u16) -> f32 { 34 | scale_by_dpi_raw(x, dpi).round().max(1.0) 35 | } 36 | -------------------------------------------------------------------------------- /crates/core/src/view/button.rs: -------------------------------------------------------------------------------- 1 | use crate::device::CURRENT_DEVICE; 2 | use crate::geom::{Rectangle, CornerSpec, BorderSpec}; 3 | use crate::font::{Fonts, font_from_style, NORMAL_STYLE}; 4 | use super::{View, Event, Hub, Bus, Id, ID_FEEDER, RenderQueue, RenderData}; 5 | use super::{THICKNESS_MEDIUM, BORDER_RADIUS_LARGE}; 6 | use crate::framebuffer::{Framebuffer, UpdateMode}; 7 | use crate::input::{DeviceEvent, FingerStatus}; 8 | use crate::gesture::GestureEvent; 9 | use crate::color::{TEXT_NORMAL, TEXT_INVERTED_HARD}; 10 | use crate::unit::scale_by_dpi; 11 | use crate::context::Context; 12 | 13 | pub struct Button { 14 | id: Id, 15 | rect: Rectangle, 16 | children: Vec>, 17 | event: Event, 18 | text: String, 19 | active: bool, 20 | pub disabled: bool, 21 | } 22 | 23 | impl Button { 24 | pub fn new(rect: Rectangle, event: Event, text: String) -> Button { 25 | Button { 26 | id: ID_FEEDER.next(), 27 | rect, 28 | children: Vec::new(), 29 | event, 30 | text, 31 | active: false, 32 | disabled: false, 33 | } 34 | } 35 | 36 | pub fn disabled(mut self, value: bool) -> Button { 37 | self.disabled = value; 38 | self 39 | } 40 | } 41 | 42 | impl View for Button { 43 | fn handle_event(&mut self, evt: &Event, _hub: &Hub, bus: &mut Bus, rq: &mut RenderQueue, _context: &mut Context) -> bool { 44 | match *evt { 45 | Event::Device(DeviceEvent::Finger { status, position, .. }) if !self.disabled => { 46 | match status { 47 | FingerStatus::Down if self.rect.includes(position) => { 48 | self.active = true; 49 | rq.add(RenderData::new(self.id, self.rect, UpdateMode::Fast)); 50 | true 51 | }, 52 | FingerStatus::Up if self.active => { 53 | self.active = false; 54 | rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui)); 55 | true 56 | }, 57 | _ => false, 58 | } 59 | }, 60 | Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => { 61 | if !self.disabled { 62 | bus.push_back(self.event.clone()); 63 | } 64 | true 65 | }, 66 | _ => false, 67 | } 68 | } 69 | 70 | fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) { 71 | let dpi = CURRENT_DEVICE.dpi; 72 | 73 | let scheme = if self.active { 74 | TEXT_INVERTED_HARD 75 | } else { 76 | TEXT_NORMAL 77 | }; 78 | let foreground = if self.disabled { scheme[2] } else { scheme[1] }; 79 | 80 | let border_radius = scale_by_dpi(BORDER_RADIUS_LARGE, dpi) as i32; 81 | let border_thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as u16; 82 | 83 | fb.draw_rounded_rectangle_with_border(&self.rect, 84 | &CornerSpec::Uniform(border_radius), 85 | &BorderSpec { thickness: border_thickness, 86 | color: foreground }, 87 | &scheme[0]); 88 | 89 | let font = font_from_style(fonts, &NORMAL_STYLE, dpi); 90 | let x_height = font.x_heights.0 as i32; 91 | let padding = font.em() as i32; 92 | let max_width = self.rect.width() as i32 - padding; 93 | 94 | let plan = font.plan(&self.text, Some(max_width), None); 95 | 96 | let dx = (self.rect.width() as i32 - plan.width) / 2; 97 | let dy = (self.rect.height() as i32 - x_height) / 2; 98 | let pt = pt!(self.rect.min.x + dx, self.rect.max.y - dy); 99 | 100 | font.render(fb, foreground, &plan, pt); 101 | } 102 | 103 | fn rect(&self) -> &Rectangle { 104 | &self.rect 105 | } 106 | 107 | fn rect_mut(&mut self) -> &mut Rectangle { 108 | &mut self.rect 109 | } 110 | 111 | fn children(&self) -> &Vec> { 112 | &self.children 113 | } 114 | 115 | fn children_mut(&mut self) -> &mut Vec> { 116 | &mut self.children 117 | } 118 | 119 | fn id(&self) -> Id { 120 | self.id 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /crates/core/src/view/clock.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Local, DateTime}; 2 | use crate::device::CURRENT_DEVICE; 3 | use crate::framebuffer::{Framebuffer, UpdateMode}; 4 | use super::{View, ViewId, Event, Hub, Bus, Id, ID_FEEDER, RenderQueue, RenderData}; 5 | use crate::gesture::GestureEvent; 6 | use crate::font::{Fonts, font_from_style, NORMAL_STYLE}; 7 | use crate::color::{BLACK, WHITE}; 8 | use crate::geom::{Rectangle}; 9 | use crate::context::Context; 10 | 11 | pub struct Clock { 12 | id: Id, 13 | rect: Rectangle, 14 | children: Vec>, 15 | format: String, 16 | time: DateTime, 17 | } 18 | 19 | impl Clock { 20 | pub fn new(rect: &mut Rectangle, context: &mut Context) -> Clock { 21 | let time = Local::now(); 22 | let format = context.settings.time_format.clone(); 23 | let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, CURRENT_DEVICE.dpi); 24 | let width = font.plan(&time.format(&format).to_string(), None, None).width + font.em() as i32; 25 | rect.min.x = rect.max.x - width; 26 | Clock { 27 | id: ID_FEEDER.next(), 28 | rect: *rect, 29 | children: Vec::new(), 30 | format, 31 | time, 32 | } 33 | } 34 | 35 | pub fn update(&mut self, rq: &mut RenderQueue) { 36 | self.time = Local::now(); 37 | rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui)); 38 | } 39 | } 40 | 41 | impl View for Clock { 42 | fn handle_event(&mut self, evt: &Event, _hub: &Hub, bus: &mut Bus, rq: &mut RenderQueue, _context: &mut Context) -> bool { 43 | match *evt { 44 | Event::ClockTick => { 45 | self.update(rq); 46 | true 47 | }, 48 | Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => { 49 | bus.push_back(Event::ToggleNear(ViewId::ClockMenu, self.rect)); 50 | true 51 | }, 52 | _ => false, 53 | } 54 | } 55 | 56 | fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) { 57 | let dpi = CURRENT_DEVICE.dpi; 58 | let font = font_from_style(fonts, &NORMAL_STYLE, dpi); 59 | let plan = font.plan(&self.time.format(&self.format).to_string(), None, None); 60 | let dx = (self.rect.width() as i32 - plan.width) / 2; 61 | let dy = (self.rect.height() as i32 - font.x_heights.0 as i32) / 2; 62 | let pt = pt!(self.rect.min.x + dx, self.rect.max.y - dy); 63 | 64 | fb.draw_rectangle(&self.rect, WHITE); 65 | font.render(fb, BLACK, &plan, pt); 66 | } 67 | 68 | fn rect(&self) -> &Rectangle { 69 | &self.rect 70 | } 71 | 72 | fn rect_mut(&mut self) -> &mut Rectangle { 73 | &mut self.rect 74 | } 75 | 76 | fn children(&self) -> &Vec> { 77 | &self.children 78 | } 79 | 80 | fn children_mut(&mut self) -> &mut Vec> { 81 | &mut self.children 82 | } 83 | 84 | fn id(&self) -> Id { 85 | self.id 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/core/src/view/filler.rs: -------------------------------------------------------------------------------- 1 | use crate::framebuffer::Framebuffer; 2 | use super::{View, Event, Hub, Bus, Id, ID_FEEDER, RenderQueue}; 3 | use crate::geom::Rectangle; 4 | use crate::context::Context; 5 | use crate::font::Fonts; 6 | use crate::color::Color; 7 | 8 | pub struct Filler { 9 | id: Id, 10 | pub rect: Rectangle, 11 | children: Vec>, 12 | color: Color, 13 | } 14 | 15 | impl Filler { 16 | pub fn new(rect: Rectangle, color: Color) -> Filler { 17 | Filler { 18 | id: ID_FEEDER.next(), 19 | rect, 20 | children: Vec::new(), 21 | color, 22 | } 23 | } 24 | } 25 | 26 | impl View for Filler { 27 | fn handle_event(&mut self, _evt: &Event, _hub: &Hub, _bus: &mut Bus, _rq: &mut RenderQueue, _context: &mut Context) -> bool { 28 | false 29 | } 30 | 31 | fn render(&self, fb: &mut dyn Framebuffer, rect: Rectangle, _fonts: &mut Fonts) { 32 | if let Some(r) = self.rect.intersection(&rect) { 33 | fb.draw_rectangle(&r, self.color); 34 | } 35 | } 36 | 37 | fn render_rect(&self, rect: &Rectangle) -> Rectangle { 38 | rect.intersection(&self.rect) 39 | .unwrap_or(self.rect) 40 | } 41 | 42 | fn rect(&self) -> &Rectangle { 43 | &self.rect 44 | } 45 | 46 | fn rect_mut(&mut self) -> &mut Rectangle { 47 | &mut self.rect 48 | } 49 | 50 | fn children(&self) -> &Vec> { 51 | &self.children 52 | } 53 | 54 | fn children_mut(&mut self) -> &mut Vec> { 55 | &mut self.children 56 | } 57 | 58 | fn id(&self) -> Id { 59 | self.id 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/core/src/view/home/directory.rs: -------------------------------------------------------------------------------- 1 | use std::path::{PathBuf, Path}; 2 | use crate::device::CURRENT_DEVICE; 3 | use crate::gesture::GestureEvent; 4 | use crate::font::{Fonts, font_from_style, NORMAL_STYLE}; 5 | use crate::color::{WHITE, BLACK, TEXT_BUMP_SMALL}; 6 | use crate::geom::{Rectangle, CornerSpec, BorderSpec}; 7 | use crate::framebuffer::Framebuffer; 8 | use crate::view::{View, Event, Hub, Bus, Id, ID_FEEDER, RenderQueue, Align}; 9 | use crate::view::{THICKNESS_SMALL, BORDER_RADIUS_SMALL}; 10 | use crate::unit::scale_by_dpi; 11 | use crate::context::Context; 12 | 13 | pub struct Directory { 14 | id: Id, 15 | rect: Rectangle, 16 | children: Vec>, 17 | pub path: PathBuf, 18 | selected: bool, 19 | align: Align, 20 | max_width: Option, 21 | } 22 | 23 | impl Directory { 24 | pub fn new(rect: Rectangle, path: PathBuf, selected: bool, align: Align, max_width: Option) -> Directory { 25 | Directory { 26 | id: ID_FEEDER.next(), 27 | rect, 28 | children: Vec::new(), 29 | path, 30 | selected, 31 | align, 32 | max_width, 33 | } 34 | } 35 | 36 | pub fn update_selected(&mut self, current_directory: &Path) -> bool { 37 | let selected = current_directory.starts_with(&self.path); 38 | self.selected = selected; 39 | selected 40 | } 41 | } 42 | 43 | impl View for Directory { 44 | fn handle_event(&mut self, evt: &Event, _hub: &Hub, bus: &mut Bus, _rq: &mut RenderQueue, _context: &mut Context) -> bool { 45 | match *evt { 46 | Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => { 47 | bus.push_back(Event::ToggleSelectDirectory(self.path.clone())); 48 | true 49 | }, 50 | _ => false, 51 | } 52 | } 53 | 54 | fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) { 55 | let dpi = CURRENT_DEVICE.dpi; 56 | fb.draw_rectangle(&self.rect, TEXT_BUMP_SMALL[0]); 57 | let font = font_from_style(fonts, &NORMAL_STYLE, dpi); 58 | let x_height = font.x_heights.0 as i32; 59 | let text = self.path.file_name().unwrap().to_string_lossy(); 60 | let plan = font.plan(text, self.max_width, None); 61 | 62 | let dx = self.align.offset(plan.width, self.rect.width() as i32); 63 | let dy = (self.rect.height() as i32 - x_height) / 2; 64 | 65 | if self.selected { 66 | let padding = font.em() as i32 / 2 - scale_by_dpi(3.0, dpi) as i32; 67 | let small_x_height = font.x_heights.0 as i32; 68 | let bg_width = plan.width + 2 * padding; 69 | let bg_height = 3 * small_x_height; 70 | let x_offset = dx - padding; 71 | let y_offset = dy + x_height - 2 * small_x_height; 72 | let pt = self.rect.min + pt!(x_offset, y_offset); 73 | let bg_rect = rect![pt, pt + pt!(bg_width, bg_height)]; 74 | let border_radius = scale_by_dpi(BORDER_RADIUS_SMALL, dpi) as i32; 75 | let border_thickness = scale_by_dpi(THICKNESS_SMALL, dpi) as u16; 76 | fb.draw_rounded_rectangle_with_border(&bg_rect, 77 | &CornerSpec::Uniform(border_radius), 78 | &BorderSpec { thickness: border_thickness, 79 | color: BLACK }, 80 | &WHITE); 81 | } 82 | 83 | let pt = pt!(self.rect.min.x + dx, self.rect.max.y - dy); 84 | font.render(fb, TEXT_BUMP_SMALL[1], &plan, pt); 85 | } 86 | 87 | fn rect(&self) -> &Rectangle { 88 | &self.rect 89 | } 90 | 91 | fn rect_mut(&mut self) -> &mut Rectangle { 92 | &mut self.rect 93 | } 94 | 95 | fn children(&self) -> &Vec> { 96 | &self.children 97 | } 98 | 99 | fn children_mut(&mut self) -> &mut Vec> { 100 | &mut self.children 101 | } 102 | 103 | fn id(&self) -> Id { 104 | self.id 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /crates/core/src/view/home/library_label.rs: -------------------------------------------------------------------------------- 1 | use crate::device::CURRENT_DEVICE; 2 | use crate::font::{Fonts, font_from_style, NORMAL_STYLE}; 3 | use crate::framebuffer::{Framebuffer, UpdateMode}; 4 | use crate::gesture::GestureEvent; 5 | use crate::color::{BLACK, WHITE}; 6 | use crate::geom::{Rectangle}; 7 | use crate::view::{View, Event, Hub, Bus, Id, ID_FEEDER, RenderQueue, RenderData, ViewId}; 8 | use crate::context::Context; 9 | 10 | pub struct LibraryLabel { 11 | id: Id, 12 | rect: Rectangle, 13 | children: Vec>, 14 | name: String, 15 | count: usize, 16 | filter: bool, 17 | } 18 | 19 | impl LibraryLabel { 20 | pub fn new(rect: Rectangle, name: &str, count: usize, filter: bool) -> LibraryLabel { 21 | LibraryLabel { 22 | id: ID_FEEDER.next(), 23 | rect, 24 | children: Vec::new(), 25 | name: name.to_string(), 26 | count, 27 | filter, 28 | } 29 | } 30 | 31 | pub fn update(&mut self, name: &str, count: usize, filter: bool, rq: &mut RenderQueue) { 32 | let mut render = false; 33 | if self.name != name { 34 | self.name = name.to_string(); 35 | render = true; 36 | } 37 | if self.count != count { 38 | self.count = count; 39 | render = true; 40 | } 41 | if self.filter != filter { 42 | self.filter = filter; 43 | render = true; 44 | } 45 | if render { 46 | rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui)); 47 | } 48 | } 49 | 50 | fn text(&self) -> String { 51 | let subject = if self.filter { 52 | if self.count != 1 { 53 | "matches" 54 | } else { 55 | "match" 56 | } 57 | } else { 58 | if self.count != 1 { 59 | "books" 60 | } else { 61 | "book" 62 | } 63 | }; 64 | 65 | if self.count == 0 { 66 | format!("{} (No {})", self.name, subject) 67 | } else { 68 | format!("{} ({} {})", self.name, self.count, subject) 69 | } 70 | } 71 | } 72 | 73 | 74 | impl View for LibraryLabel { 75 | fn handle_event(&mut self, evt: &Event, _hub: &Hub, bus: &mut Bus, _rq: &mut RenderQueue, _context: &mut Context) -> bool { 76 | match *evt { 77 | Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => { 78 | bus.push_back(Event::ToggleNear(ViewId::LibraryMenu, self.rect)); 79 | true 80 | }, 81 | _ => false, 82 | } 83 | } 84 | 85 | fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) { 86 | let dpi = CURRENT_DEVICE.dpi; 87 | let font = font_from_style(fonts, &NORMAL_STYLE, dpi); 88 | let padding = font.em() as i32 / 2; 89 | let max_width = self.rect.width().saturating_sub(2 * padding as u32) as i32; 90 | let plan = font.plan(&self.text(), Some(max_width), None); 91 | let dx = padding + (max_width - plan.width) / 2; 92 | let dy = (self.rect.height() as i32 - font.x_heights.0 as i32) / 2; 93 | let pt = pt!(self.rect.min.x + dx, self.rect.max.y - dy); 94 | fb.draw_rectangle(&self.rect, WHITE); 95 | font.render(fb, BLACK, &plan, pt); 96 | } 97 | 98 | fn rect(&self) -> &Rectangle { 99 | &self.rect 100 | } 101 | 102 | fn rect_mut(&mut self) -> &mut Rectangle { 103 | &mut self.rect 104 | } 105 | 106 | fn children(&self) -> &Vec> { 107 | &self.children 108 | } 109 | 110 | fn children_mut(&mut self) -> &mut Vec> { 111 | &mut self.children 112 | } 113 | 114 | fn id(&self) -> Id { 115 | self.id 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/core/src/view/image.rs: -------------------------------------------------------------------------------- 1 | use crate::framebuffer::{Framebuffer, UpdateMode, Pixmap}; 2 | use crate::view::{View, Event, Hub, Bus, Id, ID_FEEDER, RenderQueue, RenderData}; 3 | use crate::color::WHITE; 4 | use crate::geom::Rectangle; 5 | use crate::context::Context; 6 | use crate::font::Fonts; 7 | 8 | pub struct Image { 9 | id: Id, 10 | rect: Rectangle, 11 | children: Vec>, 12 | pixmap: Pixmap, 13 | } 14 | 15 | impl Image { 16 | pub fn new(rect: Rectangle, pixmap: Pixmap) -> Image { 17 | Image { 18 | id: ID_FEEDER.next(), 19 | rect, 20 | children: Vec::new(), 21 | pixmap, 22 | } 23 | } 24 | 25 | pub fn update(&mut self, pixmap: Pixmap, rq: &mut RenderQueue) { 26 | self.pixmap = pixmap; 27 | rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui)); 28 | } 29 | } 30 | 31 | impl View for Image { 32 | fn handle_event(&mut self, _evt: &Event, _hub: &Hub, _bus: &mut Bus, _rq: &mut RenderQueue, _context: &mut Context) -> bool { 33 | false 34 | } 35 | 36 | fn render(&self, fb: &mut dyn Framebuffer, rect: Rectangle, _fonts: &mut Fonts) { 37 | let x0 = self.rect.min.x + (self.rect.width() - self.pixmap.width) as i32 / 2; 38 | let y0 = self.rect.min.y + (self.rect.height() - self.pixmap.height) as i32 / 2; 39 | let x1 = x0 + self.pixmap.width as i32; 40 | let y1 = y0 + self.pixmap.height as i32; 41 | if let Some(r) = rect![self.rect.min, pt!(x1, y0)].intersection(&rect) { 42 | fb.draw_rectangle(&r, WHITE); 43 | } 44 | if let Some(r) = rect![self.rect.min.x, y0, x0, self.rect.max.y].intersection(&rect) { 45 | fb.draw_rectangle(&r, WHITE); 46 | } 47 | if let Some(r) = rect![pt!(x0, y1), self.rect.max].intersection(&rect) { 48 | fb.draw_rectangle(&r, WHITE); 49 | } 50 | if let Some(r) = rect![x1, self.rect.min.y, self.rect.max.x, y1].intersection(&rect) { 51 | fb.draw_rectangle(&r, WHITE); 52 | } 53 | if let Some(r) = rect![x0, y0, x1, y1].intersection(&rect) { 54 | let frame = r - pt!(x0, y0); 55 | fb.draw_framed_pixmap(&self.pixmap, &frame, r.min); 56 | } 57 | } 58 | 59 | fn render_rect(&self, rect: &Rectangle) -> Rectangle { 60 | rect.intersection(&self.rect) 61 | .unwrap_or(self.rect) 62 | } 63 | 64 | fn rect(&self) -> &Rectangle { 65 | &self.rect 66 | } 67 | 68 | fn rect_mut(&mut self) -> &mut Rectangle { 69 | &mut self.rect 70 | } 71 | 72 | fn children(&self) -> &Vec> { 73 | &self.children 74 | } 75 | 76 | fn children_mut(&mut self) -> &mut Vec> { 77 | &mut self.children 78 | } 79 | 80 | fn id(&self) -> Id { 81 | self.id 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/core/src/view/label.rs: -------------------------------------------------------------------------------- 1 | use crate::device::CURRENT_DEVICE; 2 | use crate::font::{Fonts, font_from_style, NORMAL_STYLE}; 3 | use super::{View, Event, Hub, Bus, Id, ID_FEEDER, RenderQueue, RenderData, Align}; 4 | use crate::gesture::GestureEvent; 5 | use crate::framebuffer::{Framebuffer, UpdateMode}; 6 | use crate::geom::Rectangle; 7 | use crate::color::TEXT_NORMAL; 8 | use crate::context::Context; 9 | 10 | pub struct Label { 11 | id: Id, 12 | rect: Rectangle, 13 | children: Vec>, 14 | text: String, 15 | align: Align, 16 | event: Option, 17 | hold_event: Option, 18 | } 19 | 20 | impl Label { 21 | pub fn new(rect: Rectangle, text: String, align: Align) -> Label { 22 | Label { 23 | id: ID_FEEDER.next(), 24 | rect, 25 | children: Vec::new(), 26 | text, 27 | align, 28 | event: None, 29 | hold_event: None, 30 | } 31 | } 32 | 33 | pub fn event(mut self, event: Option) -> Label { 34 | self.event = event; 35 | self 36 | } 37 | 38 | pub fn hold_event(mut self, event: Option) -> Label { 39 | self.hold_event = event; 40 | self 41 | } 42 | 43 | pub fn update(&mut self, text: &str, rq: &mut RenderQueue) { 44 | if self.text != text { 45 | self.text = text.to_string(); 46 | rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui)); 47 | } 48 | } 49 | } 50 | 51 | impl View for Label { 52 | fn handle_event(&mut self, evt: &Event, _hub: &Hub, bus: &mut Bus, _rq: &mut RenderQueue, _context: &mut Context) -> bool { 53 | match *evt { 54 | Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => { 55 | if let Some(event) = self.event.clone() { 56 | bus.push_back(event); 57 | } 58 | true 59 | }, 60 | Event::Gesture(GestureEvent::HoldFingerShort(center, _)) if self.rect.includes(center) => { 61 | if let Some(event) = self.hold_event.clone() { 62 | bus.push_back(event); 63 | } 64 | true 65 | }, 66 | _ => false, 67 | } 68 | } 69 | 70 | fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) { 71 | let dpi = CURRENT_DEVICE.dpi; 72 | 73 | fb.draw_rectangle(&self.rect, TEXT_NORMAL[0]); 74 | 75 | let font = font_from_style(fonts, &NORMAL_STYLE, dpi); 76 | let x_height = font.x_heights.0 as i32; 77 | let padding = font.em() as i32; 78 | let max_width = self.rect.width() as i32 - padding; 79 | 80 | let plan = font.plan(&self.text, Some(max_width), None); 81 | 82 | let dx = self.align.offset(plan.width, self.rect.width() as i32); 83 | let dy = (self.rect.height() as i32 - x_height) / 2; 84 | let pt = pt!(self.rect.min.x + dx, self.rect.max.y - dy); 85 | 86 | font.render(fb, TEXT_NORMAL[1], &plan, pt); 87 | } 88 | 89 | fn resize(&mut self, rect: Rectangle, _hub: &Hub, _rq: &mut RenderQueue, _context: &mut Context) { 90 | if let Some(Event::ToggleNear(_, ref mut event_rect)) = self.event.as_mut() { 91 | *event_rect = rect; 92 | } 93 | self.rect = rect; 94 | } 95 | 96 | fn rect(&self) -> &Rectangle { 97 | &self.rect 98 | } 99 | 100 | fn rect_mut(&mut self) -> &mut Rectangle { 101 | &mut self.rect 102 | } 103 | 104 | fn children(&self) -> &Vec> { 105 | &self.children 106 | } 107 | 108 | fn children_mut(&mut self) -> &mut Vec> { 109 | &mut self.children 110 | } 111 | 112 | fn id(&self) -> Id { 113 | self.id 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /crates/core/src/view/labeled_icon.rs: -------------------------------------------------------------------------------- 1 | use crate::framebuffer::Framebuffer; 2 | use crate::view::{View, Event, Hub, Bus, Id, ID_FEEDER, RenderQueue, Align}; 3 | use crate::view::icon::Icon; 4 | use crate::view::label::Label; 5 | use crate::geom::Rectangle; 6 | use crate::font::Fonts; 7 | use crate::context::Context; 8 | 9 | #[derive(Debug)] 10 | pub struct LabeledIcon { 11 | id: Id, 12 | rect: Rectangle, 13 | children: Vec>, 14 | event: Event, 15 | } 16 | 17 | impl LabeledIcon { 18 | pub fn new(name: &str, rect: Rectangle, event: Event, text: String) -> LabeledIcon { 19 | let id = ID_FEEDER.next(); 20 | let mut children = Vec::new(); 21 | let side = rect.height() as i32; 22 | 23 | let icon = Icon::new(name, 24 | rect![rect.min.x, rect.min.y, 25 | rect.min.x + side, rect.max.y], 26 | Event::Validate); 27 | children.push(Box::new(icon) as Box); 28 | 29 | let label = Label::new(rect![rect.min.x + side, rect.min.y, 30 | rect.max.x, rect.max.y], 31 | text, 32 | Align::Left(0)) 33 | .event(Some(Event::Validate)); 34 | children.push(Box::new(label) as Box); 35 | 36 | LabeledIcon { 37 | id, 38 | rect, 39 | children, 40 | event, 41 | } 42 | } 43 | 44 | pub fn update(&mut self, text: &str, rq: &mut RenderQueue) { 45 | if let Some(label) = self.children[1].downcast_mut::