portapack-mayhem/firmware/common/ui_widget.cpp
gullradriel 1dbfc50dbe
Stable merge v2.2.0 (#2736)
* Update README.md

Actually valid complete ali h4m link

* Fake brightness reimplementation  (#2444)

* Fake brightness reimplementation
* indentation
* added call to the function which is caching the display settings values
* use cached values instead of pmem

* app manager (#2442)

* fix unset autostart
* clean up - add comments to prevent misleading
* move the app to external and with necessary changes
* replace autostart app

* Add + - buttons in Encoder dial settings (#2447)

* M10 additional parser (#2448)

* Remember previous capture settings (#2450)

* Renamed parameters in rx_capture.ini file (#2452)

* Rename settings in file to match screen
* Renamed variables for hopefully better clarity

* Navigation buttons (#2458)

* regenerate bitmap data
* pagination in submenu
* using little font so we are not eating menu buttons

* docker improvements (#2455)

* Update README.md

New metal case link

* The gerber files of the portapack H4 (#2463)

* Create README.txt

* Update README.txt

* Add files via upload

The gerber files of the portapack h4.

* delete

* Upload the gerber files for H4

* Update README.md (#2456)

Added a link to Lab401.com as a purchase option for EU customers.
(Lab401 was added as the EU exclusive distributor for the H4M - https://opensourcesdrlab.com/pages/distributors)

* Update README.md

* rename bitmaps into bmp

* disabling button on main menu, change labels and add 'icons' (#2466)

* disabling button on main menu, change labels and add 'icons'

* fix reverse order of buttons

---------

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* fix for empty text prompt crash (#2468)

* added discord server information (#2471)

* Add the feature to decide rotate direction of encoder (#2472)

* Update README.md

Lab401 link with our redirection

* Fix encoder setting p.mem issue (#2475)

* Update README.md

Discord badge was broken, switching to shields.io

* Add fast flash script for sdcard switch hardware (#2480)

* fix cmake_minimum_required to 3.16 (#2499)

* fix fallthrough warning (#2497)

* Flipper tx: use file_path, example file (#2496)

* added subghz_dir
* use subghz_dir from file_path
* example file

* Externalize antenna calc and wav view (#2498)

* externalize antenna calc and wav view
* Added a tool to check if all the pictures in graphics are used in internal apps

* APRS: add frequency settings for Brazil (#2494)

* Add frequency settings for Brazil, named 'BR' that tunes to 145.570 MHz.
* Also added: Japan: 144.640 MHz (JAP), Thailand: 144.900 MHz (THA), Philippines: 144.740 MHz (PHI)
* Reordered list by increasing frequency
Co-authored-by: gullradriel <3157857+gullradriel@users.noreply.github.com>

* Added different modulations in signal generator (#2492)

* Added DSB, AM 100% mod index and AM 50% mod index. Changed UI.

* put back app in 'Utilities' (#2500)

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* fix baseband (#2501)

* externalize wipe sdcard (#2502)

* Removing vim swap files (#2503)

* Removing vim swap files
* Added vim swap file to .gitignore

* Add modal to turn off screen when charging is detected (#2514)

* Moved country-specific FREQMAN files to the separate repository set up for it. (#2517)

* fix docker build warnings in dockerfile-nogit (#2518)

* Add new app "hopper" app. (#2482)

* make both jammer and hopper exist
* add example hopper payload
* example files
* swap scanner and recon app location

* Add widget preview tool (#2520)

* PoC

* opt

* opt

* Playlist editor (#2506)

* make both exist
* format
* fix focusing issue
* add example hopper payload
* fix compiler err
* clean up
* correct linker script addr
* lint
* PoC
* unknown: write_line issue
* clean up
* merge
* fix read line
* remove debug code
* fix english
* support new file
* support enter delay
* fix crash
* remove debug code
* some final tune

* Support Bug Key AKA Auto Key for OOK Editor app (#2523)

* _

* some final tune

* textual

* rename following gull's suggestion

* add cursor to font viewer app (#2528)

* Allow disable/enable waveform in Audio app to remove decoding problem on some frequencies

* Added different modulations in signal generator

* Added DSB, AM 100% mod index and AM 50% mod index. 
* Changed UI.
* Added pulsed CW

* Adding Wefax demodulation mode inside Audio App (#2539)

* Adding_new_WFAX_GUI_mode_Audio_App

* Wefax_APT_demodulation_structure

* Solving REC Apt signal.wav from WFAX

* clang format issues

* correcting comments

* Breakout - The Portapack remake game rises from the pirate's lair (#2541)

* Breakout - The Portapack remake game rises from the pirate's lair

* Fixes

* Added a signature

* Trivial textual change about missing SD content (#2542)

* _

* _

* waveform fix 3 and trivial change (#2540)

* Moved games to new game menu (#2544)

* Moved games to new game menu
* There's enough games to have a menu now and I plan to make more. Having them in "Utilities" made no sense.

* Wefax warning fix modulation fix (#2543)

* changed order of modulations, changed case to avoid capture
* added missing AMAudioFMApt mode to dump pmem
* reorder demod, adding missing ones, fix warnings
* removed uneeded 'previous_modulation', renamed WFAX to AMFM to match other places
* removing uneeded 'previous_modulation' uneeded check in change_modulation
* move capture at the end so AMAUdioFMApt is matching the other arrays for position 4
* added AMFM to Recon Level and Scanner

* clang + more details in some comments

---------

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* Snake (#2549)

* Create the Shopping Cart Lock app

Will demonstrate tomorrow. Don't merge until I do 😁

* Fixes for HTotoo's comments 😎

* Improved audio the best I can.

If nobody has any ideas to further improve high frequencies of the audio, the hardware may not be capable. I still need to check with line-out to better speaker to make sure it's not just the speaker, but it shouldn't be.

* Compared against baseband_api.cpp - matched some things better but still playback seems to be missing higher fq sounds

* renamed wav files to a more specific / less generic name

* indentation + using variables instead of litteral names for wav files to use

* indentation

* Made a Snake game - enjoy

* Code formatting. I always forget.

* move to keep sort order

* Update external.ld

Sorry I should have also asked if there was any reason that address ranges 0xADDA0000--0xADDD0000 were skipped in external.ld.  I assumed there wasn't so I changed it to be consecutive using the same 0x10000 step as the other modules.  If there is any reason to skip them then we should add a comment to note it.  Of course these are all just temporary address values used for linking and get overwritten by a kludgy "search & replace" during the build process.

Resolves enhancement request #764

---------

Co-authored-by: gullradriel <gullradriel@no-mail.com>
Co-authored-by: Mark Thompson <129641948+NotherNgineer@users.noreply.github.com>

* Nested Debug menu into Utilities menu to tidy Home screen (#2551)

* rename rand pwd app (#2552)

* _

* remove wip code that forgot to add in old branch

* Moved speaker 1px to the right to match muted icon variant. (#2554)

* Stopwatch external app (#2553)

* updated bitmaps for speaker icon (#2555)

* updated bitmaps for speaker icon
* removed opera cake icon that was added by mistake
* adding missing Game menu source png
* cyan for Game menu
* regenerated icons

* Breakout icon change (#2556)

- Changed Breakout icon
- Changed Tetris icon color to green

* Snake icon change (#2557)

* Remove deprecated QR Code code (#2558)

* Setting for faster Button Repeat delays (#2559)

* Setting for faster Button Repeat delays

* Tweak fast delay times

* Tweak delay times

* Added description line and tweaked delay again

* OokBrute app opt (#2561)

* zooming_spectrum_AMFM_mode (#2565)

* Update README.md

Fixing opesourcesdrlab link

* Update README.md

Other link broken

* WeFax rx ext app (#2566)

* wf3

* Ookbrute (#2354)

* Revert "Ookbrute (#2354)"

This reverts commit abb8143eec.

* fix

* test edition

* re enable ble

* re enable ert

* steal amfm stuff

* something happens

* save bmp on start btn

* kinda works

* exit crash fixed

* redline, remove some hardcoded

* removed cpu killer red line, and some fixes

* simplify #1

* seems ok. time to improve

* added hidden freq offset to receiver model, so wefax can be set to the "correct" freq without users needs to substract 300 hz

* badly implemented sync detection, and disabled it.

* fix for fix

* fixes

* fix offset to real life off

* no line on freq enter

* fixes

* Doom - Mayhem Edition (#2570)

* Doom - Mini Mayhem version

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Doom - Mayhem Edition

Made the Doom - Mayhem Edition game. Some little bugs but good enough for nightly.

* Code formatting. Forgot like always.

* give more initial ammo until i put ammo around the maze to collect

* Update Doom main.cpp for better icon (#2575)

* fix external app address list (#2573)

* Add WEFAX freqman file (#2567)

* Add WEFAX freqman file
* fix bad escaped spaces and unicode characters

* Add icon for the doom game (#2574)

* Add icon for the doom game
* Add b/w .png to convert as doom icon

* Added wefax offset to audio app too. (#2572)

* added wefax offset to audio app too.

* moved from head to cpp

* Put ticker class and pp_colors in hpp file in namespace and remove helper files (#2577)

* stopwatch opt (#2578)

* stopwatch opt

* comments

* format

* fxi ms display when user tune display level

* issue template fine tune (#2579)

* Combined cpp files, stuffed helper files in hpp, updates start and game over screens (#2583)

* Combine cpp, move helpers to hpp (#2584)

* naming space (#2585)

* Tetris: Combined cpp files. Helper files into hpp. Dark mode. Encoder on. (#2587)

* Adding_Waterfall_ZOOM_x2_in_AM_modes_Audio_App (#2586)

* adding zoom_factor to app settings
* separated zoom_factor settings for AM and AMFM
* fix order so zoom factor is also applied correctly on modulation change
* fix zoom not applied when changing bandwidth
* temporary disable the Scanner so we are not breaking the nightly. Until we are choosing to finally remove it or find a better solution

---------

Co-authored-by: gullradriel <3157857+gullradriel@users.noreply.github.com>
Co-authored-by: gullradriel <gullradriel@no-mail.com>

* Fixed the I Tetromino rotation using SRS (Super Rotation System) (#2588)

* Externalize dump pmem (#2590)

* initial commit

* clang

* memory icon

* text output and exit button, FOCUS OVERRIDE TO AVOID COMPILATION ERROR

* modem and data_structure_version accessor

---------

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* remove dead code (#2593)

* remove not yet enabled screening app (#2594)

* move default splash into sdcard (#2595)

* move bmps to sdcard
* remove unrelated files
* gitignore
* credit
* format

* Externalize scanner (#2589)

* externalize scanner
* NFM as main baseband as it's the biggest used one
* fix modulation bug introduced with AMFM

* Externalize level (#2596)

* removing ability to focus on RSSI bars and to launch level app, until a solution to launch external apps from internal ones is given
* externalize Level app

* Remove unneeded AMFM support in those apps (#2597)

* Create CODE_OF_CONDUCT.md

* Create pull_request_template.md

* Create SECURITY.md

* Create CONTRIBUTING.md (#2598)

* Force 433.92 and remove metadata check (#2599)

* Force 433.92 and remove metadata check: we already know the frequency for all files so don't need a million metadata files to match.
* Variable fixes and move to header

* Added more Wefax stations (#2600)

* Delete sdcard/FREQMAN/WEFAX.TXT

Replace with 2 files

* Add more WeFax frequencies

* Update comments

* Oops wrong folder

* Oops wrong folder

* Change comments

* Remove RF TX and use PATX baseband for audio --> speaker out only (#2601)

* Force 433.92 and remove metadata check

We already know the frequency for all files so don't need a million metadata files to match.

* Remove RF TX. Improve PATX baseband.

* code formatting of course

* Issue template again (#2602)

* test1

* test2

* add tap tempo to metronomic app (#2605)

* _

* format

* fix new tree in Arch

* solving_Audio_App_AM_GUI_Problem_issue_2604 (#2609)

* make the ptext_prompt func can define which keyboard to enter (#2608)

* _

* format

* use define

* prevent long life var for audio app - AM (#2610)

* static vars so no external linkage is possible

* persistent settings and no more global living variables

---------

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* theme fix again (#2611)

* theme fix again

* _

* user can disable battery change hint (#2612)

* theme fix again

* _

* _

* GFX EQ App (#2607)

* Make the beginnings of rf3d
* Name change...
* Add mood button
* Remove forced amp settings and add persistent user settings
* Fix options bar layout and SettingsManager
* Make the background paint to black again after opening fq modal
* fix audio/mod/settings and cleaned unneeded parts
* Mapped bars to audio spectrum
* Improved frequency response... still needs work i think
* add on_freqchg to be able to answer to serial frequency change command
* Made calculations for 14 bars to fit screen and little adjustments
* Visual improvements
Co-authored-by: gullradriel

* Improved make_bitmap tool (#2615)

* Enhance Graphic Equalizer Visualization with Improved Frequency Bands and Response (#2614)

* Custom waterfall colors (#2617)

* Custom waterfall gradient
* Installing a custom waterfall gradient via fileman
* default file for user friendly swap
Co-authored-by: gullradriel <gullradriel@no-mail.com>

* Delete dead code in ble_rx_app.cpp (#2620)

Duplicate include on line 24 removed.

* Fix default waterfall file (#2621)

* correct default settings from file
* correct colors names

* fix comments (#2622)

* Touch on waterfall to set cursor pos (#2624)

* init

* fix typo that found by Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Adding 8.33KHz spacing (#2628)

* Adding 8.33KHz spacing
* remove extern options_db_t freqman_steps which is now brought by ui_receiver.hpp
* use freqman db steps instead of static array
Co-authored-by: gullradriel <gullradriel@no-mail.com>

* Added menu group for transceivers (#2623)

* Added menu group for transceivers

* Reorder apps icons

* Support IPS screen & brightness set for IPS screen (#2629)

* _

* format

* format

* format

* Fix bug that created by PR "Added menu group for transceivers" (#2630)

* spectrum cursor opt again (#2634)

* spectrum cursor opt

* fmt

* remove blink

* remove End event

* cleanup

* Update README.md

Links were dead for opensourcesdrlab

* Fix for #2538 (#2635)

* Fix for #2538

Fix for #2538
Added on_bandwidth_changed Callback to ui_transmitter.hpp
Modified the field_bw.on_change lambda in the TransmitterView constructor to trigger the on_bandwidth_changed callback
Connected the Callback in ui_siggen.cpp

I am not a C++ programmer so this change was proposed by Gemini AI.

I have built and tested the App and it works as expected and I don't think the change will have any unexpected side effects.

* Fix clang issues

Fix clang issues

* Update ui_transmitter.cpp

typo

* Revised change

The proposed change mirrors the way a change to the frequency (on_edit_frequency) is triggered in ui_siggen by the tx_view.
The bw parameter is not passed because it is stored in _setting in the tx_view and will be read by update_config.
A change to the bw is not checked against auto_update to keep its behaviour consistent with a change to the gain, amplitude or frequency.

* Make changes to the channel_bandwidth dynamic whist playing

Behaviour of channel bandwidth is now consistent with frequency, amp and gain.

* comment edit

* revert hackrf submodule checkpoint to the repo

* comment

---------

Co-authored-by: zxkmm <zxkmm@hotmail.com>

* Adding_WFM_AM_mode_to_Audio_App (#2644)

* Adding_WFM_AM_mode_to_Audio_App
* more precise values for cos and sin theta, fix sen_theta to sin_theta
* fix sen_theta to sin_theta

* going back to WFM as main baseband in main.cpp as NFM is now making the apps crashing. Looks like last additions to WFM made it bigger. (#2646)

* remove dead code (#2647)

* Update ui_tetris.cpp (#2650)

Start "I" tetromino a bit higher and block rotate if it will cause out of bounds collision.

* Noaa apt decoder (#2648)

* Explicit_naming_wefax_NOAA_and_small_addition (#2651)

* slightly improved ads-b receiver module (#2649)

* slightly improved ads-b receiver module:
* fix Heading, Speed and Vrate decoders
* decode more ModeS messages
* log all ModeS messages (except DF11)
* fix formatting (clang-style); advice on data alignment taken into account
* ADS-B module: convert Indicated AirSpeed to True AirSpeed if altitute is known
* ADS-B rx module: replacing floating point with integer arithmetic

* adding 10Hz and 50Hz to freqman_steps (#2652)

* Improve_RF_sensitivity_NOAA_signal (#2654)

* Upload the PCB file of PortaPack H4 and update the schematic file (#2657)

* Create README.txt

* Update README.txt

* Add files via upload

The gerber files of the portapack h4.

* delete

* Upload the gerber files for H4

* Create README.txt

This is the V1.0 version PCB file of PortaPack H4.

* Upload the pcb file for H4

* Update README.txt

* Update LCD_TF_Schematic.pdf

* Jammer app add modes (#2659)

* Add new jammer modes

Overview

This PR enhances the PortaPack Jammer app by introducing eight new signal types, ported from my Flipper Zero RF Jammer app (https://github.com/RocketGod-git/flipper-zero-rf-jammer). These modes expand the app's capability to disrupt a wide range of RF communication protocols, from analog radios to modern digital systems. The implementation preserves the original app structure, resolves namespace conflicts, and ensures compatibility with the Mayhem firmware.

New Modes

The following modes have been added to the options_type in ui_jammer.hpp, with corresponding signal generation in proc_jammer.cpp:

Noise: Generates broadband white noise to interfere with analog and digital signals (e.g., Wi-Fi, Bluetooth, key fobs). Highly effective for overwhelming receivers across a frequency range.

Sine: Produces a continuous, unmodulated sine wave to jam narrowband receivers, ideal for analog FM/AM radios or telemetry systems.

Square: Emits a harmonic-rich square wave, disrupting digital protocols (e.g., OOK, ASK) and systems sensitive to sharp transitions, such as remote keyless entry.

Sawtooth (Experimental): Generates a sawtooth wave with a unique harmonic profile, useful for testing interference against PWM-based or niche analog systems.

Triangle (Experimental): Creates a triangle wave with minimal harmonics, suitable for exploratory jamming of narrowband systems or receiver linearity testing.

Chirp: Outputs a rapid frequency-sweeping chirp signal, effective against frequency-hopping and spread-spectrum systems (e.g., some Wi-Fi, Bluetooth, or military radios).

Gauss: Generates Gaussian noise to mimic natural interference, targeting digital systems like GPS or data links by degrading signal-to-noise ratios.

Brute (Experimental): Transmits a constant maximum-amplitude signal to saturate simple receiver front-ends, useful for brute-force jamming of basic analog devices.

* Add new jammer modes

Overview

This PR enhances the PortaPack Jammer app by introducing eight new signal types, ported from my Flipper Zero RF Jammer app (https://github.com/RocketGod-git/flipper-zero-rf-jammer). These modes expand the app's capability to disrupt a wide range of RF communication protocols, from analog radios to modern digital systems. The implementation preserves the original app structure, resolves namespace conflicts, and ensures compatibility with the Mayhem firmware.

New Modes

The following modes have been added to the options_type in ui_jammer.hpp, with corresponding signal generation in proc_jammer.cpp:

Noise: Generates broadband white noise to interfere with analog and digital signals (e.g., Wi-Fi, Bluetooth, key fobs). Highly effective for overwhelming receivers across a frequency range.

Sine: Produces a continuous, unmodulated sine wave to jam narrowband receivers, ideal for analog FM/AM radios or telemetry systems.

Square: Emits a harmonic-rich square wave, disrupting digital protocols (e.g., OOK, ASK) and systems sensitive to sharp transitions, such as remote keyless entry.

Sawtooth (Experimental): Generates a sawtooth wave with a unique harmonic profile, useful for testing interference against PWM-based or niche analog systems.

Triangle (Experimental): Creates a triangle wave with minimal harmonics, suitable for exploratory jamming of narrowband systems or receiver linearity testing.

Chirp: Outputs a rapid frequency-sweeping chirp signal, effective against frequency-hopping and spread-spectrum systems (e.g., some Wi-Fi, Bluetooth, or military radios).

Gauss: Generates Gaussian noise to mimic natural interference, targeting digital systems like GPS or data links by degrading signal-to-noise ratios.

Brute (Experimental): Transmits a constant maximum-amplitude signal to saturate simple receiver front-ends, useful for brute-force jamming of basic analog devices.

* refactor the serial log logic of BLE Rx (#2660)

* Prepare for display orientation part 1 (#2661)

* fix png part

* screen max width fixes (#2663)

* max width fixes

* format

* Audio to right (#2664)

* r.align

* Storing_selected_NOAA_filter_in_settings_file (#2665)

* Storing_selected_NOAA_filter_in_settings_file

* format_issues

* wfm_filters_GUI_name_std (#2668)

* getres cmd (#2671)

* ui new coord system examples and macros (#2672)

* Detector RX ext app (#2673)

* Jammer improvements (#2674)

* Add new jammer modes
Overview:
This PR enhances the PortaPack Jammer app by introducing eight new signal types, ported from my Flipper Zero RF Jammer app (https://github.com/RocketGod-git/flipper-zero-rf-jammer). These modes expand the app's capability to disrupt a wide range of RF communication protocols, from analog radios to modern digital systems. The implementation preserves the original app structure, resolves namespace conflicts, and ensures compatibility with the Mayhem firmware.

New Modes

The following modes have been added to the options_type in ui_jammer.hpp, with corresponding signal generation in proc_jammer.cpp:

Noise: Generates broadband white noise to interfere with analog and digital signals (e.g., Wi-Fi, Bluetooth, key fobs). Highly effective for overwhelming receivers across a frequency range.

Sine: Produces a continuous, unmodulated sine wave to jam narrowband receivers, ideal for analog FM/AM radios or telemetry systems.

Square: Emits a harmonic-rich square wave, disrupting digital protocols (e.g., OOK, ASK) and systems sensitive to sharp transitions, such as remote keyless entry.

Sawtooth (Experimental): Generates a sawtooth wave with a unique harmonic profile, useful for testing interference against PWM-based or niche analog systems.

Triangle (Experimental): Creates a triangle wave with minimal harmonics, suitable for exploratory jamming of narrowband systems or receiver linearity testing.

Chirp: Outputs a rapid frequency-sweeping chirp signal, effective against frequency-hopping and spread-spectrum systems (e.g., some Wi-Fi, Bluetooth, or military radios).

Gauss: Generates Gaussian noise to mimic natural interference, targeting digital systems like GPS or data links by degrading signal-to-noise ratios.

Brute (Experimental): Transmits a constant maximum-amplitude signal to saturate simple receiver front-ends, useful for brute-force jamming of basic analog devices.

* Fixed and made brutal.

This PR introduces user-focused improvements to the Jammer App in the HackRF PortaPack Mayhem Firmware, enhancing usability and flexibility. The changes address specific user requirements for a more intuitive default configuration, continuous waveform support, and dynamic setting adjustments during transmission.

* jammer fix (#2676)

* jammer fix

* Adding_BPF_selection_to_the_NOAA_APT_signal (#2675)

* Adding_BPF_selection_to_the_NOAA_APT_signal
* comments, spell mistake .

* trivial apps folder movement (#2677)

* Clean_LCD_beat_in_NOAA_Rx_App (#2678)

* Added ability to enter custom tone values in Morse app (#2679)

* Added ability to enter custom tone values in Morse app

Added the ability to type in a custom tone value in the morse TX app (issue#2582)

*Click on the tone field to open a keyboard for entering in a desired value between 100hz - 9999hz.

*Maintains original step value of 20 when scrolling the rotary wheel.

* Update ui_morse.cpp

Replaced std::to_string with to_string_dec_uint

* Moved tone_input_buffer init to in-class

* removed some std stuff only used here (#2681)

* 80mhz jammer range (#2682)

Looks great 😎🤘🚀

* Radio app improvements (#2680)

* Rename looking glass preset for clarity and consistency (#2686)

* Gfx widget and Radio (#2685)

* widgetize
* gfx and Radio improvement
* format + handle not wfm visual states
* wf or gf

* Externalize widget (#2688)

* Add all jammer modes in hopper app (#2691)

Added all modes that jammer app supports in hopper app.

* Super secret dont look (#2690)

* Add new jammer modes

Overview

This PR enhances the PortaPack Jammer app by introducing eight new signal types, ported from my Flipper Zero RF Jammer app (https://github.com/RocketGod-git/flipper-zero-rf-jammer). These modes expand the app's capability to disrupt a wide range of RF communication protocols, from analog radios to modern digital systems. The implementation preserves the original app structure, resolves namespace conflicts, and ensures compatibility with the Mayhem firmware.

New Modes

The following modes have been added to the options_type in ui_jammer.hpp, with corresponding signal generation in proc_jammer.cpp:

Noise: Generates broadband white noise to interfere with analog and digital signals (e.g., Wi-Fi, Bluetooth, key fobs). Highly effective for overwhelming receivers across a frequency range.

Sine: Produces a continuous, unmodulated sine wave to jam narrowband receivers, ideal for analog FM/AM radios or telemetry systems.

Square: Emits a harmonic-rich square wave, disrupting digital protocols (e.g., OOK, ASK) and systems sensitive to sharp transitions, such as remote keyless entry.

Sawtooth (Experimental): Generates a sawtooth wave with a unique harmonic profile, useful for testing interference against PWM-based or niche analog systems.

Triangle (Experimental): Creates a triangle wave with minimal harmonics, suitable for exploratory jamming of narrowband systems or receiver linearity testing.

Chirp: Outputs a rapid frequency-sweeping chirp signal, effective against frequency-hopping and spread-spectrum systems (e.g., some Wi-Fi, Bluetooth, or military radios).

Gauss: Generates Gaussian noise to mimic natural interference, targeting digital systems like GPS or data links by degrading signal-to-noise ratios.

Brute (Experimental): Transmits a constant maximum-amplitude signal to saturate simple receiver front-ends, useful for brute-force jamming of basic analog devices.

* Super secret

* You gotta get (Get) that (That) dirt off your shoulder

* Add 1ms hop option to hopper app + 0ms (freeze UI) (#2692)

* add dark theme (#2695)

* Made the Dino Game (#2697)

* Add vendor name in bluetooth rx app (#2696)

* add macaddress db, add vendor name in bluetooth rx app

* show "missing macaddress.db" instead of unknown if db not found

* bluetooth rx list with colors based on mac vendor

* bug fix

* Modified Text Editor to handle long presses. (#2698)

* Improved FPV_ANALOG.txt FREQMAN file (#2700)

* Improved FPV_ANALOG.txt FREQMAN file
Removed unused or super rare analog fpv bands: U, O, H, D
Added 1.2GHz -1.3GHz channels sometimes used for long range analog fpv


* Corrected and updated the labels to be more consistent.

Corrected the labels to be more consistent.

I also somehow messed up the correct channels because 1.3GHz FPV is not fully standardized, but these channels seem to be the most common.
It should be all correct now.

Example transmitters using those channels:
https://greenchip.com.ua/0-0-1615-2.html
https://flymod.net/en/item/walksnail_vtx_9ch
https://pl.aliexpress.com/item/1005006505365351.html

* Filemanager: go to parent directory keep track of the right selected … (#2702)

* Filemanager: go to parent directory keep track of the right selected item and page number
* review: avoid unnecessary copies in get_extension

* ADSB database update (tools, db) (#2701)

* enhance make_airlines_db tool
* enhance make_icao24_db tool
* update airlinescode (.txt, .db), aircraftdatabase/icao24 (.csv, .db)

* Made the Space Invaders game. Argh matey! (#2709)

* Made the Space Invaders game. Argh matey!
* Format code, sigh.

* Made the Blackjack game (#2712)

* Made the Blackjack game
* Format Blackjack main.cpp
* Changed spade to diamond for dark mode visibility
* Format code

* Update app icons for Space Invaders and Dino Game (#2713)

* BLE Rx Improvements (#2710)

* Work to allow for unique beacon parsing functions.
* Fix Copyright
* Update firmware/application/apps/ble_rx_app.cpp
* Update firmware/baseband/proc_btlerx.cpp
* PR suggestions.
* Fix String.
* Refactor

* Added 3d printed cases for the H4M (#2715)

* Battleship (#2720)

* Made the Battleship 2P 2PP game - FSK is wip
* Using POCSAG

* Adding simple FSK Rx Processor. Can be used with New Apps. (#2716)

* Work to allow for unique beacon parsing functions.

* Fixing pull.

* Changes.

* Formatting.

* Fix Copyright

* Update firmware/application/apps/ble_rx_app.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update firmware/baseband/proc_btlerx.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* PR suggestions.

* Fix String.

* FSK Rx Improvements. Works for my custom protocol.

* Fix buffer size.

* Refactor

* Formatting.

* Formatting.

* Fixing compiling, and BLE Rx UI/Performance.

* More improvements.

* Fixing stuck state.

* More stuck parsing fix.

* Combining PR changes.

* Improvements from previous PR.

* Fix dbM calculation relative to device RSSI.

* Formatting.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: TJ <tj.baginski@cognosos.com>

* Add blue bar to subghzd+weather (#2724)

* AIS map improv (#2725)

* AIS map improv

* format code mismatch with vc

* Add radio settings, new app icon, and other UI improvements (#2732)

* update submodule (#2734)

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* update version (#2735)

Co-authored-by: gullradriel <gullradriel@no-mail.com>

---------

Co-authored-by: Erwin Ried <1091420+eried@users.noreply.github.com>
Co-authored-by: hackrfstuff <leszczyleszczy@icloud.com>
Co-authored-by: sommermorgentraum <24917424+zxkmm@users.noreply.github.com>
Co-authored-by: Totoo <ttotoo@gmail.com>
Co-authored-by: Mark Thompson <129641948+NotherNgineer@users.noreply.github.com>
Co-authored-by: E.T. <tamas@eisenberger.hu>
Co-authored-by: OpenSourceSDRLab <opensourcesdr@outlook.com>
Co-authored-by: quantum-x <simon.yorkston@gmail.com>
Co-authored-by: gullradriel <gullradriel@no-mail.com>
Co-authored-by: Lucas C. Villa Real <lucasvr@users.noreply.github.com>
Co-authored-by: Davide Rovelli <103165301+daviderud@users.noreply.github.com>
Co-authored-by: Gaurav Chaturvedi <oddtazz@users.noreply.github.com>
Co-authored-by: RocketGod <57732082+RocketGod-git@users.noreply.github.com>
Co-authored-by: Lerold <github@lerold.slmail.me>
Co-authored-by: Brumi-2021 <86470699+Brumi-2021@users.noreply.github.com>
Co-authored-by: dark-juju <2839275+dark-juju@users.noreply.github.com>
Co-authored-by: Benjamin Møller <37707273+LupusE@users.noreply.github.com>
Co-authored-by: Oleg Belousov <belousov.oleg@gmail.com>
Co-authored-by: haruk <104354987+exe-noisy@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Luca <61653175+iu2frl@users.noreply.github.com>
Co-authored-by: Richard <richard.toy@gmail.com>
Co-authored-by: zxkmm <zxkmm@hotmail.com>
Co-authored-by: horrordash <45861453+horrordash@users.noreply.github.com>
Co-authored-by: Alien <2142224+mythic-alien@users.noreply.github.com>
Co-authored-by: Petro Danylevskyi <petro@danylevskyi.com>
Co-authored-by: Tommaso Ventafridda <33782489+tomventa@users.noreply.github.com>
Co-authored-by: Netro <146584182+iNetro@users.noreply.github.com>
Co-authored-by: plomek <86431917+plomek@users.noreply.github.com>
Co-authored-by: TJ <tj.baginski@cognosos.com>
2025-07-11 16:33:21 +02:00

3115 lines
80 KiB
C++

/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2025 RocketGod
* Copyright (C) 2025 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_widget.hpp"
#include "ui_painter.hpp"
#include "portapack.hpp"
#include <cstdint>
#include <cstddef>
#include <algorithm>
#include "chprintf.h"
#include "irq_controls.hpp"
#include "string_format.hpp"
#include "usb_serial_device_to_host.h"
#include "rtc_time.hpp"
#include "battery.hpp"
using namespace portapack;
using namespace rtc_time;
namespace ui {
static bool ui_dirty = true;
void dirty_set() {
ui_dirty = true;
}
void dirty_clear() {
ui_dirty = false;
}
bool is_dirty() {
return ui_dirty;
}
/* Widget ****************************************************************/
const std::vector<Widget*> Widget::no_children{};
Point Widget::screen_pos() {
return screen_rect().location();
}
Size Widget::size() const {
return _parent_rect.size();
}
Rect Widget::screen_rect() const {
return parent() ? (parent_rect() + parent()->screen_pos()) : parent_rect();
}
Rect Widget::parent_rect() const {
return _parent_rect;
}
void Widget::set_parent_rect(const Rect new_parent_rect) {
_parent_rect = new_parent_rect;
set_dirty();
}
Widget* Widget::parent() const {
return parent_;
}
void Widget::set_parent(Widget* const widget) {
if (widget == parent_) {
return;
}
if (parent_ && !widget) {
// We have a parent, but are losing it. Update visible status.
dirty_overlapping_children_in_rect(screen_rect());
visible(false);
}
if (widget == nullptr)
on_before_detach();
parent_ = widget;
if (widget != nullptr)
on_after_attach();
set_dirty();
}
void Widget::set_dirty() {
flags.dirty = true;
dirty_set();
}
bool Widget::dirty() const {
return flags.dirty;
}
void Widget::set_clean() {
flags.dirty = false;
}
void Widget::hidden(bool hide) {
if (hide != flags.hidden) {
flags.hidden = hide;
// If parent is hidden, either of these is a no-op.
if (hide) {
// TODO: Instead of dirtying parent entirely, dirty only children
// that overlap with this widget.
// parent()->dirty_overlapping_children_in_rect(parent_rect());
/* TODO: Notify self and all non-hidden children that they're
* now effectively hidden?
*/
} else {
set_dirty();
/* TODO: Notify self and all non-hidden children that they're
* now effectively shown?
*/
}
}
}
void Widget::focus() {
context().focus_manager().set_focus_widget(this);
}
void Widget::on_focus() {
}
void Widget::blur() {
context().focus_manager().set_focus_widget(nullptr);
}
void Widget::on_blur() {
}
bool Widget::focusable() const {
return flags.focusable;
}
void Widget::set_focusable(const bool value) {
flags.focusable = value;
}
bool Widget::has_focus() {
return (context().focus_manager().focus_widget() == this);
}
bool Widget::on_key(const KeyEvent event) {
(void)event;
return false;
}
bool Widget::on_encoder(const EncoderEvent event) {
(void)event;
return false;
}
bool Widget::on_touch(const TouchEvent event) {
(void)event;
return false;
}
bool Widget::on_keyboard(const KeyboardEvent event) {
(void)event;
return false;
}
const std::vector<Widget*>& Widget::children() const {
return no_children;
}
Context& Widget::context() const {
chDbgAssert(parent_, "parent_ is null",
"Check that parent isn't null before deref.");
return parent()->context();
}
void Widget::set_style(const Style* new_style) {
if (new_style != style_) {
style_ = new_style;
set_dirty();
}
}
const Style& Widget::style() const {
return style_ ? *style_ : parent()->style();
}
void Widget::visible(bool v) {
if (v != flags.visible) {
flags.visible = v;
/* TODO: This on_show/on_hide implementation seems inelegant.
* But I need *some* way to take/configure resources when
* a widget becomes visible, and reverse the process when the
* widget becomes invisible, whether the widget (or parent) is
* hidden, or the widget (or parent) is removed from the tree.
*/
if (v) {
on_show();
} else {
on_hide();
// Set all children invisible too.
for (const auto child : children()) {
child->visible(false);
}
}
}
}
bool Widget::highlighted() const {
return flags.highlighted;
}
void Widget::set_highlighted(const bool value) {
flags.highlighted = value;
}
void Widget::dirty_overlapping_children_in_rect(const Rect& child_rect) {
for (auto child : children()) {
if (!child_rect.intersect(child->parent_rect()).is_empty()) {
child->set_dirty();
}
}
}
void Widget::getAccessibilityText(std::string& result) {
result = "";
}
void Widget::getWidgetName(std::string& result) {
result = "";
}
/* View ******************************************************************/
void View::paint(Painter& painter) {
painter.fill_rectangle(
screen_rect(),
style().background);
}
void View::add_child(Widget* const widget) {
if (widget) {
if (widget->parent() == nullptr) {
widget->set_parent(this);
children_.push_back(widget);
}
}
}
void View::add_children(const std::initializer_list<Widget*> children) {
children_.insert(std::end(children_), children);
for (auto child : children) {
child->set_parent(this);
}
}
void View::remove_child(Widget* const widget) {
if (widget) {
children_.erase(std::remove(children_.begin(), children_.end(), widget), children_.end());
widget->set_parent(nullptr);
}
}
void View::remove_children(const std::vector<Widget*>& children) {
for (auto child : children) {
remove_child(child);
}
}
const std::vector<Widget*>& View::children() const {
return children_;
}
std::string View::title() const {
return "";
};
/* OptionTabView *********************************************************/
OptionTabView::OptionTabView(Rect parent_rect) {
set_parent_rect(parent_rect);
add_child(&check_enable);
hidden(true);
check_enable.on_select = [this](Checkbox&, bool value) {
enabled = value;
};
}
void OptionTabView::set_enabled(bool value) {
check_enable.set_value(value);
}
bool OptionTabView::is_enabled() {
return check_enable.value();
}
void OptionTabView::set_type(std::string type) {
check_enable.set_text("Transmit " + type);
}
void OptionTabView::focus() {
check_enable.focus();
}
/* Rectangle *************************************************************/
Rectangle::Rectangle(
Color c)
: Widget{},
color{c} {
}
Rectangle::Rectangle(
Rect parent_rect,
Color c)
: Widget{parent_rect},
color{c} {
}
void Rectangle::set_color(const Color c) {
color = c;
set_dirty();
}
void Rectangle::set_outline(const bool outline) {
_outline = outline;
set_dirty();
}
void Rectangle::paint(Painter& painter) {
if (!_outline) {
painter.fill_rectangle(
screen_rect(),
color);
} else {
painter.draw_rectangle(
screen_rect(),
color);
}
}
/* Text ******************************************************************/
Text::Text(
Rect parent_rect,
std::string text)
: Widget{parent_rect},
text{std::move(text)} {
}
Text::Text(
Rect parent_rect)
: Text{parent_rect, {}} {
}
void Text::set(std::string_view value) {
text = std::string{value};
set_dirty();
}
void Text::getAccessibilityText(std::string& result) {
result = text;
}
void Text::getWidgetName(std::string& result) {
result = "Text";
}
void Text::paint(Painter& painter) {
const auto rect = screen_rect();
auto s = has_focus() ? style().invert() : style();
auto max_len = (unsigned)rect.width() / s.font.char_width();
auto text_view = std::string_view{text};
painter.fill_rectangle(rect, s.background);
if (text_view.length() > max_len)
text_view = text_view.substr(0, max_len);
painter.draw_string(
rect.location(),
s,
text_view);
}
/* Labels ****************************************************************/
Labels::Labels(
std::initializer_list<Label> labels)
: labels_{labels} {
}
void Labels::set_labels(std::initializer_list<Label> labels) {
labels_ = labels;
set_dirty();
}
void Labels::paint(Painter& painter) {
for (auto& label : labels_) {
painter.draw_string(
label.pos + screen_pos(),
style().font,
label.color,
style().background,
label.text);
}
}
void Labels::getAccessibilityText(std::string& result) {
result = "";
for (auto& label : labels_) {
result += label.text;
result += ", ";
}
}
void Labels::getWidgetName(std::string& result) {
result = "Labels";
}
/* LiveDateTime **********************************************************/
void LiveDateTime::on_tick_second() {
rtc_time::now(datetime);
text = "";
if (!hide_clock) {
if (date_enabled) {
text = to_string_dec_uint(datetime.year(), 4, '0') + "-" +
to_string_dec_uint(datetime.month(), 2, '0') + "-" +
to_string_dec_uint(datetime.day(), 2, '0') + " ";
} else {
text = " ";
}
text = text + to_string_dec_uint(datetime.hour(), 2, '0') + ":" + to_string_dec_uint(datetime.minute(), 2, '0');
if (seconds_enabled) {
text += ":";
if (init_delay == 0)
text += to_string_dec_uint(datetime.second(), 2, '0');
else {
// Placeholder while the seconds are not updated
text += "XX";
init_delay--;
}
}
}
set_dirty();
}
LiveDateTime::LiveDateTime(
Rect parent_rect)
: Widget{parent_rect} {
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
this->on_tick_second();
};
}
LiveDateTime::~LiveDateTime() {
rtc_time::signal_tick_second -= signal_token_tick_second;
}
void LiveDateTime::paint(Painter& painter) {
const auto rect = screen_rect();
const auto s = style();
on_tick_second();
painter.fill_rectangle(rect, s.background);
painter.draw_string(
rect.location(),
s,
text);
}
void LiveDateTime::set_hide_clock(bool new_value) {
this->hide_clock = new_value;
}
void LiveDateTime::set_date_enabled(bool new_value) {
this->date_enabled = new_value;
}
void LiveDateTime::set_seconds_enabled(bool new_value) {
this->seconds_enabled = new_value;
}
/* BigFrequency **********************************************************/
BigFrequency::BigFrequency(
Rect parent_rect,
rf::Frequency frequency)
: Widget{parent_rect},
_frequency{frequency} {
}
void BigFrequency::set(const rf::Frequency frequency) {
_frequency = frequency;
set_dirty();
}
void BigFrequency::paint(Painter& painter) {
uint32_t i, digit_def;
std::array<char, 7> digits;
char digit;
Point digit_pos;
ui::Color segment_color;
if (_frequency != _previous_frequency) {
_previous_frequency = _frequency;
rf::Frequency frequency{_frequency};
const auto rect = screen_rect();
// Erase
painter.fill_rectangle(
{{0, rect.location().y()}, {screen_width, 52}},
Theme::getInstance()->bg_darkest->background);
// Prepare digits
if (!frequency) {
digits.fill(10); // ----.---
digit_pos = {0, rect.location().y()};
} else {
frequency /= 1000; // GMMM.KKK(uuu)
for (i = 0; i < 7; i++) {
digits[6 - i] = frequency % 10;
frequency /= 10;
}
// Remove leading zeros
for (i = 0; i < 3; i++) {
if (!digits[i])
digits[i] = 16; // "Don't draw" code
else
break;
}
digit_pos = {(Coord)(240 - ((7 * digit_width) + 8) - (i * digit_width)) / 2, rect.location().y()};
}
segment_color = style().foreground;
// Draw
for (i = 0; i < 7; i++) {
digit = digits[i];
if (digit < 16) {
digit_def = segment_font[(uint8_t)digit];
for (size_t s = 0; s < 7; s++) {
if (digit_def & 1)
painter.fill_rectangle({digit_pos + segments[s].location(), segments[s].size()}, segment_color);
digit_def >>= 1;
}
}
if (i == 3) {
// Dot
painter.fill_rectangle({digit_pos + Point(34, 48), {4, 4}}, segment_color);
digit_pos += {(digit_width + 8), 0};
} else {
digit_pos += {digit_width, 0};
}
}
}
}
/* ProgressBar ***********************************************************/
ProgressBar::ProgressBar(
Rect parent_rect)
: Widget{parent_rect} {
}
void ProgressBar::set_max(const uint32_t max) {
if (max == _max) return;
if (_value > _max)
_value = _max;
_max = max;
set_dirty();
}
void ProgressBar::set_value(const uint32_t value) {
if (value == _value) return;
if (value > _max)
_value = _max;
else
_value = value;
set_dirty();
}
void ProgressBar::getAccessibilityText(std::string& result) {
result = to_string_dec_uint(_value) + " / " + to_string_dec_uint(_max);
}
void ProgressBar::getWidgetName(std::string& result) {
result = "ProgressBar";
}
void ProgressBar::paint(Painter& painter) {
int v_scaled;
const auto sr = screen_rect();
const auto s = style();
v_scaled = (sr.size().width() * (uint64_t)_value) / _max;
painter.fill_rectangle({sr.location(), {v_scaled, sr.size().height()}}, style().foreground);
painter.fill_rectangle({{sr.location().x() + v_scaled, sr.location().y()}, {sr.size().width() - v_scaled, sr.size().height()}}, s.background);
painter.draw_rectangle(sr, s.foreground);
}
/* ActivityDot ***********************************************************/
ActivityDot::ActivityDot(
Rect parent_rect,
Color color)
: Widget{parent_rect},
_color{color} {}
void ActivityDot::paint(Painter& painter) {
painter.fill_rectangle(screen_rect(), _on ? _color : Theme::getInstance()->bg_medium->background);
}
void ActivityDot::toggle() {
_on = !_on;
set_dirty();
}
void ActivityDot::reset() {
_on = false;
set_dirty();
}
/* Console ***************************************************************/
Console::Console(
Rect parent_rect)
: Widget{parent_rect} {
}
void Console::clear(bool clear_buffer = false) {
if (clear_buffer)
buffer.clear();
if (!hidden() && visible()) {
display.fill_rectangle(
screen_rect(),
Theme::getInstance()->bg_darkest->background);
}
pos = {0, 0};
}
void Console::write(std::string message) {
bool escape = false;
if (!hidden() && visible()) {
const Style& s = style();
const Font& font = s.font;
auto rect = screen_rect();
ui::Color pen_color = s.foreground;
for (auto c : message) {
if (escape) {
if (c < std::size(term_colors))
pen_color = term_colors[(uint8_t)c];
else
pen_color = s.foreground;
escape = false;
} else {
if (c == '\n') {
crlf();
} else if (c == '\r') {
pos = {0, pos.y()};
} else if (c == '\x1B') {
escape = true;
} else {
auto glyph = font.glyph(c);
auto advance = glyph.advance();
// Would drawing next character be off the end? Newline.
if ((pos.x() + advance.x()) > rect.width())
crlf();
Point pos_glyph{
rect.left() + pos.x(),
display.scroll_area_y(pos.y())};
display.draw_glyph(pos_glyph, glyph, pen_color, s.background);
pos += {advance.x(), 0};
}
}
}
buffer = message;
} else {
if (buffer.size() < 256) buffer += message;
}
}
void Console::getAccessibilityText(std::string& result) {
result = "{" + buffer + "}";
}
void Console::getWidgetName(std::string& result) {
result = "Console";
}
void Console::writeln(std::string message) {
write(message + "\n");
}
void Console::paint(Painter&) {
write(buffer);
}
void Console::on_show() {
enable_scrolling(true);
clear();
}
bool Console::scrolling_enabled = false;
void Console::enable_scrolling(bool enable) {
if (enable) {
auto sr = screen_rect();
auto line_height = style().font.line_height();
// Count full lines that can fit in console's rectangle.
auto max_lines = sr.height() / line_height; // NB: int division to floor.
// The scroll area must be a multiple of the line_height
// or some lines will end up vertically truncated.
scroll_height = max_lines * line_height;
display.scroll_set_area(sr.top(), sr.top() + scroll_height);
display.scroll_set_position(0);
scrolling_enabled = true;
} else {
display.scroll_disable();
scrolling_enabled = false;
}
}
void Console::on_hide() {
/* TODO: Clear region to eliminate brief flash of content at un-shifted
* position? */
enable_scrolling(false);
}
void Console::crlf() {
if (hidden() || !visible()) return;
const auto& s = style();
auto sr = screen_rect();
auto line_height = s.font.line_height();
// Advance to the next line (\n) position and "carriage return" x to 0.
pos = {0, pos.y() + line_height};
if (pos.y() >= scroll_height) {
// Line is past off the "bottom", need to scroll.
if (!scrolling_enabled)
enable_scrolling(true);
// See the notes in lcd_ili9341.hpp about how scrolling works.
// The gist is that VSA will be moved to scroll the "top" off the
// screen. The drawing code uses 'scroll_area_y' to get the actual
// screen coordinate based on VSA. The "bottom" line is *always*
// at 'VSA + ((max_lines - 1) * line_height)' and so is constant.
pos = {0, scroll_height - line_height};
// Scroll off the "top" line.
display.scroll(-line_height);
// Clear the new line at the "bottom".
Rect dirty{sr.left(), display.scroll_area_y(pos.y()), sr.width(), line_height};
display.fill_rectangle(dirty, s.background);
}
}
/* Checkbox **************************************************************/
Checkbox::Checkbox(
Point parent_pos,
size_t length,
std::string text,
bool small)
: Widget{},
text_{text},
small_{small} {
if (!small_)
set_parent_rect({parent_pos, {static_cast<ui::Dim>((8 * length) + 24), 24}});
else
set_parent_rect({parent_pos, {static_cast<ui::Dim>((8 * length) + 16), 16}});
set_focusable(true);
}
void Checkbox::set_text(const std::string value) {
text_ = value;
set_dirty();
}
bool Checkbox::set_value(const bool value) {
value_ = value;
set_dirty();
if (on_select) {
on_select(*this, value_);
return true;
}
return false;
}
void Checkbox::getAccessibilityText(std::string& result) {
result = text_ + ((value_) ? " checked" : " unchecked");
}
void Checkbox::getWidgetName(std::string& result) {
result = "Checkbox";
}
bool Checkbox::value() const {
return value_;
}
void Checkbox::paint(Painter& painter) {
const auto r = screen_rect();
const auto paint_style = (has_focus() || highlighted()) ? style().invert() : style();
const auto x = r.location().x();
const auto y = r.location().y();
const auto label_r = paint_style.font.size_of(text_);
if (!small_) {
painter.draw_rectangle({{r.location()}, {24, 24}}, style().foreground);
painter.fill_rectangle({x + 1, y + 1, 24 - 2, 24 - 2}, style().background);
// Highlight
painter.draw_rectangle({x + 2, y + 2, 24 - 4, 24 - 4}, paint_style.background);
if (value_ == true) {
// Check
portapack::display.draw_line({x + 2, y + 14}, {x + 6, y + 18}, Theme::getInstance()->fg_green->foreground);
portapack::display.draw_line({x + 6, y + 18}, {x + 20, y + 4}, Theme::getInstance()->fg_green->foreground);
} else {
// Cross
portapack::display.draw_line({x + 1, y + 1}, {x + 24 - 2, y + 24 - 2}, Theme::getInstance()->fg_red->foreground);
portapack::display.draw_line({x + 24 - 2, y + 1}, {x + 1, y + 24 - 2}, Theme::getInstance()->fg_red->foreground);
}
painter.draw_string(
{static_cast<Coord>(x + 24 + 4),
static_cast<Coord>(y + (24 - label_r.height()) / 2)},
paint_style,
text_);
} else {
painter.draw_rectangle({{r.location()}, {16, 16}}, style().foreground);
painter.fill_rectangle({x + 1, y + 1, 16 - 2, 16 - 2}, style().background);
// Highlight
painter.draw_rectangle({x + 1, y + 1, 16 - 2, 16 - 2}, paint_style.background);
if (value_ == true) {
// Check
portapack::display.draw_line({x + 2, y + 8}, {x + 6, y + 12}, Theme::getInstance()->fg_green->foreground);
portapack::display.draw_line({x + 6, y + 12}, {x + 13, y + 5}, Theme::getInstance()->fg_green->foreground);
} else {
// Cross
portapack::display.draw_line({x + 1, y + 1}, {x + 16 - 2, y + 16 - 2}, Theme::getInstance()->fg_red->foreground);
portapack::display.draw_line({x + 16 - 2, y + 1}, {x + 1, y + 16 - 2}, Theme::getInstance()->fg_red->foreground);
}
painter.draw_string(
{static_cast<Coord>(x + 16 + 2),
static_cast<Coord>(y + (16 - label_r.height()) / 2)},
paint_style,
text_);
}
}
bool Checkbox::on_key(const KeyEvent key) {
if (key == KeyEvent::Select)
return set_value(not value_);
return false;
}
bool Checkbox::on_keyboard(const KeyboardEvent event) {
if (event == 10 || event == 32) return set_value(not value_);
return false;
}
bool Checkbox::on_touch(const TouchEvent event) {
switch (event.type) {
case TouchEvent::Type::Start:
set_highlighted(true);
set_dirty();
return true;
case TouchEvent::Type::End:
set_highlighted(false);
value_ = not value_;
set_dirty();
if (on_select) {
on_select(*this, value_);
}
return true;
default:
return false;
}
}
/* Button ****************************************************************/
Button::Button(
Rect parent_rect,
std::string text,
bool instant_exec)
: Widget{parent_rect},
text_{text},
instant_exec_{instant_exec} {
set_focusable(true);
}
void Button::set_text(const std::string value) {
text_ = value;
set_dirty();
}
std::string Button::text() const {
return text_;
}
void Button::getAccessibilityText(std::string& result) {
result = text_;
}
void Button::getWidgetName(std::string& result) {
result = "Button";
}
void Button::paint(Painter& painter) {
Color bg, fg;
const auto r = screen_rect();
if (has_focus() || highlighted()) {
bg = style().foreground;
fg = Theme::getInstance()->bg_darkest->background;
} else {
bg = Theme::getInstance()->bg_medium->background;
fg = style().foreground;
}
const Style paint_style = {style().font, bg, fg};
painter.draw_rectangle({r.location(), {r.size().width(), 1}}, Theme::getInstance()->bg_light->background);
painter.draw_rectangle({r.location().x(), r.location().y() + r.size().height() - 1, r.size().width(), 1}, Theme::getInstance()->bg_dark->background);
painter.draw_rectangle({r.location().x() + r.size().width() - 1, r.location().y(), 1, r.size().height()}, Theme::getInstance()->bg_dark->background);
painter.fill_rectangle(
{r.location().x(), r.location().y() + 1, r.size().width() - 1, r.size().height() - 2},
paint_style.background);
const auto label_r = paint_style.font.size_of(text_);
painter.draw_string(
{r.location().x() + (r.size().width() - label_r.width()) / 2, r.location().y() + (r.size().height() - label_r.height()) / 2},
paint_style,
text_);
}
void Button::on_focus() {
if (on_highlight)
on_highlight(*this);
}
bool Button::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (on_select) {
on_select(*this);
return true;
}
} else {
if (on_dir) {
return on_dir(*this, key);
}
}
return false;
}
bool Button::on_keyboard(const KeyboardEvent event) {
if (event == 10 || event == 32) {
if (on_select) {
on_select(*this);
return true;
}
}
return false;
}
bool Button::on_touch(const TouchEvent event) {
switch (event.type) {
case TouchEvent::Type::Start:
set_highlighted(true);
set_dirty();
if (on_touch_press) {
on_touch_press(*this);
}
if (on_select && instant_exec_) {
on_select(*this);
}
return true;
case TouchEvent::Type::End:
set_highlighted(false);
set_dirty();
if (on_touch_release) {
on_touch_release(*this);
}
if (on_select && !instant_exec_) {
on_select(*this);
}
return true;
default:
return false;
}
#if 0
switch(event.type) {
case TouchEvent::Type::Start:
flags.highlighted = true;
set_dirty();
return true;
case TouchEvent::Type::Move:
{
const bool new_highlighted = screen_rect().contains(event.point);
if( flags.highlighted != new_highlighted ) {
flags.highlighted = new_highlighted;
set_dirty();
}
}
return true;
case TouchEvent::Type::End:
if( flags.highlighted ) {
flags.highlighted = false;
set_dirty();
if( on_select ) {
on_select(*this);
}
}
return true;
default:
return false;
}
#endif
}
/* ButtonWithEncoder ****************************************************************/
ButtonWithEncoder::ButtonWithEncoder(
Rect parent_rect,
std::string text,
bool instant_exec)
: Widget{parent_rect},
text_{text},
instant_exec_{instant_exec} {
set_focusable(true);
}
void ButtonWithEncoder::set_text(const std::string value) {
text_ = value;
set_dirty();
}
int32_t ButtonWithEncoder::get_encoder_delta() {
return encoder_delta;
}
void ButtonWithEncoder::set_encoder_delta(const int32_t delta) {
encoder_delta = delta;
}
std::string ButtonWithEncoder::text() const {
return text_;
}
void ButtonWithEncoder::getAccessibilityText(std::string& result) {
result = text_;
}
void ButtonWithEncoder::getWidgetName(std::string& result) {
result = "ButtonWithEncoder";
}
void ButtonWithEncoder::paint(Painter& painter) {
Color bg, fg;
const auto r = screen_rect();
if (has_focus() || highlighted()) {
bg = style().foreground;
fg = Theme::getInstance()->bg_darkest->background;
} else {
bg = Theme::getInstance()->bg_medium->background;
fg = style().foreground;
}
const Style paint_style = {style().font, bg, fg};
painter.draw_rectangle({r.location(), {r.size().width(), 1}}, Theme::getInstance()->bg_light->background);
painter.draw_rectangle({r.location().x(), r.location().y() + r.size().height() - 1, r.size().width(), 1}, Theme::getInstance()->bg_dark->background);
painter.draw_rectangle({r.location().x() + r.size().width() - 1, r.location().y(), 1, r.size().height()}, Theme::getInstance()->bg_dark->background);
painter.fill_rectangle(
{r.location().x(), r.location().y() + 1, r.size().width() - 1, r.size().height() - 2},
paint_style.background);
const auto label_r = paint_style.font.size_of(text_);
painter.draw_string(
{r.location().x() + (r.size().width() - label_r.width()) / 2, r.location().y() + (r.size().height() - label_r.height()) / 2},
paint_style,
text_);
}
void ButtonWithEncoder::on_focus() {
if (on_highlight)
on_highlight(*this);
}
bool ButtonWithEncoder::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (on_select) {
on_select(*this);
return true;
}
} else {
if (on_dir) {
return on_dir(*this, key);
}
}
return false;
}
bool ButtonWithEncoder::on_keyboard(const KeyboardEvent key) {
if (key == 32 || key == 10) {
if (on_select) {
on_select(*this);
return true;
}
}
return false;
}
bool ButtonWithEncoder::on_touch(const TouchEvent event) {
switch (event.type) {
case TouchEvent::Type::Start:
set_highlighted(true);
set_dirty();
if (on_touch_press) {
on_touch_press(*this);
}
if (on_select && instant_exec_) {
on_select(*this);
}
return true;
case TouchEvent::Type::End:
set_highlighted(false);
set_dirty();
if (on_touch_release) {
on_touch_release(*this);
}
if (on_select && !instant_exec_) {
on_select(*this);
}
return true;
default:
return false;
}
#if 0
switch(event.type) {
case TouchEvent::Type::Start:
flags.highlighted = true;
set_dirty();
return true;
case TouchEvent::Type::Move:
{
const bool new_highlighted = screen_rect().contains(event.point);
if( flags.highlighted != new_highlighted ) {
flags.highlighted = new_highlighted;
set_dirty();
}
}
return true;
case TouchEvent::Type::End:
if( flags.highlighted ) {
flags.highlighted = false;
set_dirty();
if( on_select ) {
on_select(*this);
}
}
return true;
default:
return false;
}
#endif
}
bool ButtonWithEncoder::on_encoder(const EncoderEvent delta) {
if (delta != 0) {
encoder_delta += delta;
delta_change = true;
on_change();
} else
delta_change = 0;
return true;
}
/* NewButton ****************************************************************/
NewButton::NewButton(
Rect parent_rect,
std::string text,
const Bitmap* bitmap)
: NewButton{parent_rect, text, bitmap, Theme::getInstance()->fg_darkcyan->foreground} {}
NewButton::NewButton(
Rect parent_rect,
std::string text,
const Bitmap* bitmap,
Color color,
bool vertical_center)
: Widget{parent_rect},
color_{color},
text_{text},
bitmap_{bitmap},
vertical_center_{vertical_center} {
set_focusable(true);
}
void NewButton::set_text(const std::string value) {
text_ = value;
set_dirty();
}
void NewButton::getAccessibilityText(std::string& result) {
result = text_;
}
void NewButton::getWidgetName(std::string& result) {
result = "NewButton";
}
std::string NewButton::text() const {
return text_;
}
void NewButton::set_bitmap(const Bitmap* bitmap) {
bitmap_ = bitmap;
set_dirty();
}
const Bitmap* NewButton::bitmap() {
return bitmap_;
}
void NewButton::set_color(Color color) {
color_ = color;
set_dirty();
}
void NewButton::set_bg_color(Color color) {
bg_color_ = color;
set_dirty();
}
void NewButton::set_vertical_center(bool value) {
vertical_center_ = value;
set_dirty();
}
ui::Color NewButton::color() {
return color_;
}
void NewButton::paint(Painter& painter) {
if (!bitmap_ && text_.empty())
return;
const auto r = screen_rect();
const Style style = paint_style();
painter.draw_rectangle({r.location(), {r.width(), 1}}, Theme::getInstance()->bg_light->background);
painter.draw_rectangle({r.left(), r.top() + r.height() - 1, r.width(), 1}, Theme::getInstance()->bg_dark->background);
painter.draw_rectangle({r.left() + r.width() - 1, r.top(), 1, r.height()}, Theme::getInstance()->bg_dark->background);
painter.fill_rectangle(
{r.left(), r.top() + 1, r.width() - 1, r.height() - 2},
style.background);
int y = r.top();
if (bitmap_) {
int offset_y = vertical_center_ ? (r.height() / 2) - (bitmap_->size.height() / 2) : 6;
Point bmp_pos = {r.left() + (r.width() / 2) - (bitmap_->size.width() / 2), r.top() + offset_y};
y += bitmap_->size.height() - offset_y;
painter.draw_bitmap(
bmp_pos,
*bitmap_,
color_,
style.background);
}
if (!text_.empty()) {
const auto label_r = style.font.size_of(text_);
painter.draw_string(
{r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
style,
text_);
}
}
Style NewButton::paint_style() {
MutableStyle s{style()};
if (has_focus() || highlighted()) {
s.background = style().foreground;
s.foreground = Theme::getInstance()->bg_darkest->background;
} else {
s.background = bg_color_;
s.foreground = style().foreground;
}
return s;
}
void NewButton::on_focus() {
if (on_highlight)
on_highlight(*this);
}
bool NewButton::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (on_select) {
on_select();
return true;
}
} else {
if (on_dir) {
return on_dir(*this, key);
}
}
return false;
}
bool NewButton::on_keyboard(const KeyboardEvent key) {
if (key == 32 || key == 10) {
if (on_select) {
on_select();
return true;
}
}
return false;
}
bool NewButton::on_touch(const TouchEvent event) {
switch (event.type) {
case TouchEvent::Type::Start:
set_highlighted(true);
set_dirty();
return true;
case TouchEvent::Type::End:
set_highlighted(false);
set_dirty();
if (on_select) {
on_select();
}
return true;
default:
return false;
}
}
/* Image *****************************************************************/
Image::Image()
: Image{{}, nullptr, Theme::getInstance()->bg_darkest->foreground, Theme::getInstance()->bg_darkest->background} {
}
Image::Image(
const Rect parent_rect,
const Bitmap* bitmap,
const Color foreground,
const Color background)
: Widget{parent_rect},
bitmap_{bitmap},
foreground_{foreground},
background_{background} {
}
void Image::set_bitmap(const Bitmap* bitmap) {
bitmap_ = bitmap;
set_dirty();
}
void Image::set_foreground(const Color color) {
foreground_ = color;
set_dirty();
}
void Image::set_background(const Color color) {
background_ = color;
set_dirty();
}
void Image::invert_colors() {
Color temp;
temp = background_;
background_ = foreground_;
foreground_ = temp;
set_dirty();
}
void Image::paint(Painter& painter) {
if (bitmap_) {
// Code also handles ImageButton behavior.
const bool selected = (has_focus() || highlighted());
painter.draw_bitmap(
screen_pos(),
*bitmap_,
selected ? background_ : foreground_,
selected ? foreground_ : background_);
}
}
/* ImageButton ***********************************************************/
// TODO: Virtually all this code is duplicated from Button. Base class?
ImageButton::ImageButton(
const Rect parent_rect,
const Bitmap* bitmap,
const Color foreground,
const Color background)
: Image{parent_rect, bitmap, foreground, background} {
set_focusable(true);
}
void ImageButton::getAccessibilityText(std::string& result) {
result = "image";
}
void ImageButton::getWidgetName(std::string& result) {
result = "ImageButton";
}
bool ImageButton::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (on_select) {
on_select(*this);
return true;
}
}
return false;
}
bool ImageButton::on_keyboard(const KeyboardEvent key) {
if (key == 32 || key == 10) {
if (on_select) {
on_select(*this);
return true;
}
}
return false;
}
bool ImageButton::on_touch(const TouchEvent event) {
switch (event.type) {
case TouchEvent::Type::Start:
set_highlighted(true);
set_dirty();
return true;
case TouchEvent::Type::End:
set_highlighted(false);
set_dirty();
if (on_select) {
on_select(*this);
}
return true;
default:
return false;
}
}
/* ImageToggle ***********************************************************/
ImageToggle::ImageToggle(
Rect parent_rect,
const Bitmap* bitmap_)
: ImageToggle{parent_rect,
bitmap_,
Theme::getInstance()->fg_green->foreground,
Theme::getInstance()->fg_light->foreground,
Theme::getInstance()->bg_dark->background} {}
ImageToggle::ImageToggle(
Rect parent_rect,
const Bitmap* bitmap_,
Color foreground_true,
Color foreground_false,
Color background_)
: ImageToggle{parent_rect,
bitmap_,
bitmap_,
foreground_true,
background_,
foreground_false,
background_} {}
ImageToggle::ImageToggle(
Rect parent_rect,
const Bitmap* bitmap_true,
const Bitmap* bitmap_false,
Color foreground_true,
Color background_true,
Color foreground_false,
Color background_false)
: ImageButton{parent_rect, bitmap_false, foreground_false, background_false},
bitmap_true_{bitmap_true},
bitmap_false_{bitmap_false},
foreground_true_{foreground_true},
background_true_{background_true},
foreground_false_{foreground_false},
background_false_{background_false},
value_{false} {
ImageButton::on_select = [this](ImageButton&) {
set_value(!value());
};
}
bool ImageToggle::value() const {
return value_;
}
void ImageToggle::getAccessibilityText(std::string& result) {
result = value_ ? "checked" : "unchecked";
}
void ImageToggle::getWidgetName(std::string& result) {
result = "ImageToggle";
}
void ImageToggle::set_value(bool b) {
if (b == value_)
return;
value_ = b;
set_bitmap(b ? bitmap_true_ : bitmap_false_);
set_foreground(b ? foreground_true_ : foreground_false_);
set_background(b ? background_true_ : background_false_);
if (on_change)
on_change(b);
}
/* ImageOptionsField *****************************************************/
ImageOptionsField::ImageOptionsField(
Rect parent_rect,
Color foreground,
Color background,
options_t options)
: Widget{parent_rect},
options{std::move(options)},
foreground_{foreground},
background_{background} {
set_focusable(true);
}
size_t ImageOptionsField::selected_index() const {
return selected_index_;
}
size_t ImageOptionsField::selected_index_value() const {
return options[selected_index_].second;
}
void ImageOptionsField::getAccessibilityText(std::string& result) {
result = "selected index: " + to_string_dec_uint(selected_index_);
}
void ImageOptionsField::getWidgetName(std::string& result) {
result = "ImageOptionsField";
}
void ImageOptionsField::set_selected_index(const size_t new_index) {
if (new_index < options.size()) {
if (new_index != selected_index()) {
selected_index_ = new_index;
if (on_change) {
on_change(selected_index(), options[selected_index()].second);
}
set_dirty();
}
}
}
void ImageOptionsField::set_by_value(value_t v) {
size_t new_index = 0;
for (const auto& option : options) {
if (option.second == v) {
set_selected_index(new_index);
return;
}
new_index++;
}
// No exact match was found, default to 0.
set_selected_index(0);
}
void ImageOptionsField::set_options(options_t new_options) {
options = std::move(new_options);
// Set an invalid index to force on_change.
selected_index_ = (size_t)-1;
set_selected_index(0);
set_dirty();
}
void ImageOptionsField::paint(Painter& painter) {
const bool selected = (has_focus() || highlighted());
const auto paint_style = selected ? style().invert() : style();
painter.draw_rectangle(
{screen_rect().location(), {screen_rect().size().width() + 4, screen_rect().size().height() + 4}},
paint_style.background);
painter.draw_bitmap(
{screen_pos().x() + 2, screen_pos().y() + 2},
*options[selected_index_].first,
foreground_,
background_);
}
void ImageOptionsField::on_focus() {
if (on_show_options) {
on_show_options();
}
}
bool ImageOptionsField::on_encoder(const EncoderEvent delta) {
set_selected_index(selected_index() + delta);
return true;
}
bool ImageOptionsField::on_keyboard(const KeyboardEvent key) {
if (key == '+' || key == ' ' || key == 10) return on_encoder(1);
if (key == '-' || key == 8) return on_encoder(-1);
return false;
}
bool ImageOptionsField::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
}
return true;
}
/* OptionsField **********************************************************/
OptionsField::OptionsField(
Point parent_pos,
size_t length,
options_t options,
bool centered)
: Widget{{parent_pos, {8 * (int)length, 16}}},
length_{length},
options_{std::move(options)},
centered_{centered} {
set_focusable(true);
}
size_t OptionsField::selected_index() const {
return selected_index_;
}
const OptionsField::name_t& OptionsField::selected_index_name() const {
return options_[selected_index_].first;
}
const OptionsField::value_t& OptionsField::selected_index_value() const {
return options_[selected_index_].second;
}
void OptionsField::getAccessibilityText(std::string& result) {
result = "options: ";
bool first = true;
for (const auto& option : options_) {
if (!first) result += " ,";
first = false;
result += option.first;
}
result += "; selected: " + selected_index_name();
}
void OptionsField::getWidgetName(std::string& result) {
result = "OptionsField";
}
void OptionsField::set_selected_index(const size_t new_index, bool trigger_change) {
if (new_index < options_.size()) {
if (new_index != selected_index() || trigger_change) {
selected_index_ = new_index;
if (on_change) {
on_change(selected_index(), options_[selected_index()].second);
}
set_dirty();
}
}
}
void OptionsField::set_by_value(value_t v) {
size_t new_index = 0;
for (const auto& option : options_) {
if (option.second == v) {
set_selected_index(new_index);
return;
}
new_index++;
}
// No exact match was found, default to 0.
set_selected_index(0);
}
void OptionsField::set_by_nearest_value(value_t v) {
size_t new_index = 0;
size_t curr_index = 0;
int32_t min_diff = INT32_MAX;
for (const auto& option : options_) {
auto diff = abs(v - option.second);
if (diff < min_diff) {
min_diff = diff;
new_index = curr_index;
}
curr_index++;
}
set_selected_index(new_index);
}
void OptionsField::set_options(options_t new_options) {
options_ = std::move(new_options);
// Set an invalid index to force on_change.
selected_index_ = (size_t)-1;
set_selected_index(0);
set_dirty();
}
void OptionsField::paint(Painter& painter) {
const auto paint_style = has_focus() ? style().invert() : style();
painter.fill_rectangle({screen_rect().location(), {(int)length_ * 8, 16}}, Theme::getInstance()->bg_darkest->background);
if (selected_index() < options_.size()) {
std::string_view temp = selected_index_name();
if (temp.length() > length_)
temp = temp.substr(0, length_);
Point draw_pos = screen_pos();
if (centered_) {
// 8 is because big font width is 8px
// type is from: struct Point : constexpr int x() const
int text_width = temp.length() * 8;
int available_width = length_ * 8;
int x_offset = (available_width - text_width) / 2;
draw_pos = {draw_pos.x() + x_offset, draw_pos.y()};
}
painter.draw_string(
draw_pos,
paint_style,
temp);
}
}
void OptionsField::on_focus() {
if (on_show_options) {
on_show_options();
}
}
bool OptionsField::on_encoder(const EncoderEvent delta) {
int32_t new_value = selected_index() + delta;
if (new_value < 0)
new_value = options_.size() - 1;
else if ((size_t)new_value >= options_.size())
new_value = 0;
set_selected_index(new_value);
return true;
}
bool OptionsField::on_keyboard(const KeyboardEvent key) {
if (key == '+' || key == ' ' || key == 10) return on_encoder(1);
if (key == '-' || key == 8) return on_encoder(-1);
return false;
}
bool OptionsField::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
}
return true;
}
/* TextEdit ***********************************************************/
TextEdit::TextEdit(
std::string& str,
size_t max_length,
Point position,
uint32_t length)
: Widget{{position, {8 * static_cast<int>(length), 16}}},
text_{str},
max_length_{std::max<size_t>(max_length, str.length())},
char_count_{std::max<uint32_t>(length, 1)},
cursor_pos_{text_.length()},
insert_mode_{true} {
set_focusable(true);
}
const std::string& TextEdit::value() const {
return text_;
}
void TextEdit::getAccessibilityText(std::string& result) {
result = text_;
}
void TextEdit::getWidgetName(std::string& result) {
result = "TextEdit";
}
void TextEdit::set_cursor(uint32_t pos) {
cursor_pos_ = std::min<size_t>(pos, text_.length());
set_dirty();
}
void TextEdit::set_insert_mode() {
insert_mode_ = true;
}
void TextEdit::set_overwrite_mode() {
insert_mode_ = false;
}
void TextEdit::char_add(char c) {
// Don't add if inserting and at max_length and
// don't overwrite if past the end of the text.
if ((text_.length() >= max_length_ && insert_mode_) ||
(cursor_pos_ >= text_.length() && !insert_mode_))
return;
if (insert_mode_)
text_.insert(cursor_pos_, 1, c);
else
text_[cursor_pos_] = c;
cursor_pos_++;
set_dirty();
}
void TextEdit::char_delete() {
if (cursor_pos_ == 0)
return;
cursor_pos_--;
text_.erase(cursor_pos_, 1);
set_dirty();
}
void TextEdit::paint(Painter& painter) {
auto rect = screen_rect();
auto text_style = has_focus() ? style().invert() : style();
auto offset = 0;
// Does the string need to be shifted?
if (cursor_pos_ >= char_count_)
offset = cursor_pos_ - char_count_ + 1;
// Draw the text starting at the offset.
for (uint32_t i = 0; i < char_count_; i++) {
// Using draw_char to blank the rest of the line with spaces produces less flicker.
auto c = (i + offset < text_.length()) ? text_[i + offset] : ' ';
painter.draw_char(
{rect.location().x() + (static_cast<int>(i) * char_width), rect.location().y()},
text_style, c);
}
// Determine cursor position on screen (either the cursor position or the last char).
int32_t cursor_x = char_width * (offset > 0 ? char_count_ - 1 : cursor_pos_);
Point cursor_point{screen_pos().x() + cursor_x, screen_pos().y()};
auto cursor_style = text_style.invert();
// Invert the cursor character when in overwrite mode.
if (!insert_mode_ && (cursor_pos_) < text_.length())
painter.draw_char(cursor_point, cursor_style, text_[cursor_pos_]);
// Draw the cursor.
Rect cursor_box{cursor_point, {char_width, char_height}};
painter.draw_rectangle(cursor_box, cursor_style.background);
}
bool TextEdit::on_key(const KeyEvent key) {
if (key == KeyEvent::Left && cursor_pos_ > 0)
cursor_pos_--;
else if (key == KeyEvent::Right && cursor_pos_ < text_.length())
cursor_pos_++;
else if (key == KeyEvent::Select) {
if (key_is_long_pressed(key)) {
// Delete text to the cursor.
text_ = text_.substr(cursor_pos_);
set_cursor(0);
} else {
insert_mode_ = !insert_mode_;
}
} else
return false;
set_dirty();
return true;
}
bool TextEdit::on_keyboard(const KeyboardEvent key) {
// if ascii printable
if (key >= 0x20 && key <= 0x7e) {
char_add(key);
return true;
}
if (key == 8) {
char_delete();
return true;
}
return false;
}
bool TextEdit::on_encoder(const EncoderEvent delta) {
int32_t new_pos = cursor_pos_ + delta;
// Let the encoder wrap around the ends of the text.
if (new_pos < 0)
new_pos = text_.length();
else if (static_cast<size_t>(new_pos) > text_.length())
new_pos = 0;
set_cursor(new_pos);
return true;
}
bool TextEdit::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start)
focus();
set_dirty();
return true;
}
void TextEdit::on_focus() {
// Enable long press on "Select".
SwitchesState config;
config[toUType(Switch::Sel)] = true;
set_switches_long_press_config(config);
}
void TextEdit::on_blur() {
// Reset long press.
SwitchesState config{};
set_switches_long_press_config(config);
}
/* TextField *************************************************************/
TextField::TextField(Rect parent_rect, std::string text)
: Text(parent_rect, std::move(text)) {
set_focusable(true);
}
const std::string& TextField::get_text() const {
return text;
}
void TextField::getAccessibilityText(std::string& result) {
result = text;
}
void TextField::getWidgetName(std::string& result) {
result = "TextField";
}
void TextField::set_text(std::string_view value) {
set(value);
if (on_change)
on_change(*this);
}
bool TextField::on_key(KeyEvent key) {
if (key == KeyEvent::Select && on_select) {
on_select(*this);
return true;
}
return false;
}
bool TextField::on_encoder(EncoderEvent delta) {
if (on_encoder_change) {
on_encoder_change(*this, delta);
return true;
}
return false;
}
bool TextField::on_touch(TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
return true;
}
return false;
}
/* BatteryIcon *************************************************************/
BatteryIcon::BatteryIcon(Rect parent_rect, uint8_t percent)
: Widget(parent_rect) {
this->set_battery(percent <= 100 ? 1 : 0, percent, false);
set_focusable(true);
}
void BatteryIcon::getAccessibilityText(std::string& result) {
result = to_string_dec_uint(percent_) + "%";
}
void BatteryIcon::getWidgetName(std::string& result) {
result = "Battery percent";
}
void BatteryIcon::set_battery(uint8_t valid_mask, uint8_t percentage, bool charge) {
if (charge == charge_ && percent_ == percentage && valid_ == valid_mask) return;
percent_ = percentage;
charge_ = charge;
valid_ = valid_mask;
if ((valid_mask & battery::BatteryManagement::BATT_VALID_VOLTAGE) != battery::BatteryManagement::BATT_VALID_VOLTAGE) percent_ = 102; // to indicate error
set_dirty();
}
bool BatteryIcon::on_key(KeyEvent key) {
if (key == KeyEvent::Select && on_select) {
on_select();
return true;
}
return false;
}
bool BatteryIcon::on_touch(TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
return true;
}
if (event.type == TouchEvent::Type::End && on_select) {
on_select();
return true;
}
return false;
}
void BatteryIcon::paint(Painter& painter) {
ui::Rect rect = screen_rect(); // 10, 1 * 16
painter.fill_rectangle(rect, has_focus() || highlighted() ? Theme::getInstance()->fg_light->foreground : Theme::getInstance()->bg_dark->background); // clear
ui::Color battColor = (charge_) ? Theme::getInstance()->fg_blue->foreground : Theme::getInstance()->fg_green->foreground;
// batt body:
painter.draw_vline({rect.left() + 1, rect.top() + 2}, rect.height() - 4, battColor);
painter.draw_vline({rect.right() - 2, rect.top() + 2}, rect.height() - 4, battColor);
painter.draw_hline({rect.left() + 1, rect.top() + 2}, rect.width() - 2, battColor);
painter.draw_hline({rect.left() + 1, rect.bottom() - 2}, rect.width() - 2, battColor);
// batt cap:
painter.draw_hline({rect.left() + 3, rect.top() + 1}, rect.width() - 6, battColor);
painter.draw_hline({rect.left() + 3, 0}, rect.width() - 6, battColor);
if (percent_ > 100) { // error / unk
painter.draw_string({rect.left() + 2, rect.top() + 3}, font::fixed_5x8, Theme::getInstance()->bg_dark->foreground, Theme::getInstance()->bg_dark->background, "?");
return;
}
int8_t ppx = (rect.bottom() - 3) - (rect.top() + 2); // 11px max height to draw bars
int8_t ptd = (int8_t)((static_cast<float>(percent_) / 100.0f) * (float)ppx + 0.5); // pixels to draw
int8_t pp = ppx - ptd; // pixels to start from
if (percent_ >= 70)
battColor = Theme::getInstance()->fg_green->foreground;
else if (percent_ >= 40)
battColor = Theme::getInstance()->fg_orange->foreground;
else
battColor = Theme::getInstance()->fg_red->foreground;
// fill the bars
for (int y = pp; y < ppx; y++) {
painter.draw_hline({rect.left() + 2, rect.top() + 3 + y}, rect.width() - 4, battColor);
}
}
/* BatteryTextField *************************************************************/
BatteryTextField::BatteryTextField(Rect parent_rect, uint8_t percent)
: Widget(parent_rect) {
this->set_battery(percent <= 100 ? 1 : 0, percent, false);
set_focusable(true);
}
void BatteryTextField::paint(Painter& painter) {
Color bg = has_focus() || highlighted() ? Theme::getInstance()->fg_light->foreground : Theme::getInstance()->bg_dark->background;
ui::Rect rect = screen_rect(); // 2 * 8, 1 * 16
painter.fill_rectangle(rect, bg); // clear
std::string txt_batt = percent_ <= 100 ? to_string_dec_uint(percent_) : "UNK";
int xdelta = 0;
if (txt_batt.length() == 1)
xdelta = 5;
else if (txt_batt.length() == 2)
xdelta = 2;
painter.draw_string({rect.left() + xdelta, rect.top()}, font::fixed_5x8, Theme::getInstance()->bg_dark->foreground, bg, txt_batt);
painter.draw_string({rect.left(), rect.top() + 8}, font::fixed_5x8, Theme::getInstance()->bg_dark->foreground, bg, (charge_) ? "+%" : " %");
}
void BatteryTextField::getAccessibilityText(std::string& result) {
result = to_string_dec_uint(percent_) + "%";
}
void BatteryTextField::getWidgetName(std::string& result) {
result = "Battery percent";
}
void BatteryTextField::set_battery(uint8_t valid_mask, uint8_t percentage, bool charge) {
if (charge == charge_ && percent_ == percentage && valid_ == valid_mask) return;
charge_ = charge;
percent_ = percentage;
valid_ = valid_mask;
if ((valid_mask & battery::BatteryManagement::BATT_VALID_VOLTAGE) != battery::BatteryManagement::BATT_VALID_VOLTAGE) percent_ = 102; // to indicate error
set_dirty();
}
bool BatteryTextField::on_key(KeyEvent key) {
if (key == KeyEvent::Select && on_select) {
on_select();
return true;
}
return false;
}
bool BatteryTextField::on_touch(TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
return true;
}
if (event.type == TouchEvent::Type::End && on_select) {
on_select();
return true;
}
return false;
}
/* NumberField ***********************************************************/
NumberField::NumberField(
Point parent_pos,
int length,
range_t range,
int32_t step,
char fill_char,
bool can_loop)
: Widget{{parent_pos, {8 * length, 16}}},
range{range},
step{step},
length_{length},
fill_char{fill_char},
can_loop{can_loop} {
set_focusable(true);
}
int32_t NumberField::value() const {
return value_;
}
void NumberField::getAccessibilityText(std::string& result) {
result = to_string_dec_int(value_);
}
void NumberField::getWidgetName(std::string& result) {
result = "NumberField";
}
void NumberField::set_value(int32_t new_value, bool trigger_change) {
if (can_loop) {
if (new_value >= range.first)
new_value = new_value % (range.second + 1);
else
new_value = range.second + new_value + 1;
}
new_value = clip(new_value, range.first, range.second);
if (new_value != value()) {
value_ = new_value;
if (on_change && trigger_change) {
on_change(value_);
}
set_dirty();
}
}
void NumberField::set_range(const int32_t min, const int32_t max) {
range.first = min;
range.second = max;
set_value(value(), false);
}
void NumberField::set_step(const int32_t new_step) {
step = new_step;
}
void NumberField::paint(Painter& painter) {
const auto text = to_string_dec_int(value_, length_, fill_char);
const auto paint_style = has_focus() ? style().invert() : style();
painter.draw_string(
screen_pos(),
paint_style,
text);
}
bool NumberField::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (on_select) {
on_select(*this);
return true;
}
}
return false;
}
bool NumberField::on_encoder(const EncoderEvent delta) {
int32_t old_value = value();
set_value(value() + (delta * step));
if (on_wrap) {
if ((delta > 0) && (value() < old_value))
on_wrap(1);
else if ((delta < 0) && (value() > old_value))
on_wrap(-1);
}
return true;
}
bool NumberField::on_keyboard(const KeyboardEvent key) {
if (key == 10) {
if (on_select) {
on_select(*this);
return true;
}
}
if (key == '+' || key == ' ') {
return on_encoder(1);
}
if (key == '-' || key == 8) {
return on_encoder(-1);
}
return false;
}
bool NumberField::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
}
return true;
}
/* SymField **************************************************************/
SymField::SymField(
Point parent_pos,
size_t length,
Type type,
bool explicit_edits)
: Widget{{parent_pos, {char_width * (int)length, 16}}},
type_{type},
explicit_edits_{explicit_edits} {
if (length == 0)
length = 1;
selected_ = length - 1;
value_.resize(length);
switch (type) {
case Type::Oct:
set_symbol_list("01234567");
break;
case Type::Dec:
set_symbol_list("0123456789");
break;
case Type::Hex:
set_symbol_list("0123456789ABCDEF");
break;
case Type::Alpha:
set_symbol_list(" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
break;
default:
set_symbol_list("01");
break;
}
set_focusable(true);
}
SymField::SymField(
Point parent_pos,
size_t length,
std::string symbol_list,
bool explicit_edits)
: SymField{parent_pos, length, Type::Custom, explicit_edits} {
set_symbol_list(std::move(symbol_list));
}
char SymField::get_symbol(size_t index) const {
if (index >= value_.length())
return 0;
return value_[index];
}
void SymField::set_symbol(size_t index, char symbol) {
if (index >= value_.length())
return;
set_symbol_internal(index, ensure_valid(symbol));
}
size_t SymField::get_offset(size_t index) const {
if (index >= value_.length())
return 0;
// NB: Linear search - symbol lists are small.
return symbols_.find(value_[index]);
}
void SymField::set_offset(size_t index, size_t offset) {
if (index >= value_.length() || offset >= symbols_.length())
return;
set_symbol_internal(index, symbols_[offset]);
}
void SymField::set_symbol_list(std::string symbol_list) {
if (symbol_list.length() == 0)
return;
symbols_ = std::move(symbol_list);
ensure_all_symbols();
}
void SymField::set_value(uint64_t value) {
auto v = value;
uint8_t radix = get_radix();
for (int i = value_.length() - 1; i >= 0; --i) {
uint8_t temp = v % radix;
value_[i] = uint_to_char(temp, radix);
v /= radix;
}
if (on_change)
on_change(*this);
}
void SymField::set_value(std::string_view value) {
// Is new value too long?
// TODO: Truncate instead? Which end?
if (value.length() > value_.length())
return;
// Right-align string in field.
auto left_padding = value_.length() - value.length();
value_ = std::string(static_cast<size_t>(left_padding), '\0') + std::string{value};
ensure_all_symbols();
}
uint64_t SymField::to_integer() const {
uint64_t v = 0;
uint64_t mul = 1;
uint8_t radix = get_radix();
for (int i = value_.length() - 1; i >= 0; --i) {
auto temp = char_to_uint(value_[i], radix);
v += temp * mul;
mul *= radix;
}
return v;
}
const std::string& SymField::to_string() const {
return value_;
}
void SymField::getAccessibilityText(std::string& result) {
result = value_;
}
void SymField::getWidgetName(std::string& result) {
result = "SymField";
}
void SymField::paint(Painter& painter) {
Point p = screen_pos();
for (size_t n = 0; n < value_.length(); n++) {
auto c = value_[n];
MutableStyle paint_style{style()};
// Only highlight while focused.
if (has_focus()) {
if (explicit_edits_) {
// Invert the whole field on focus if explicit edits is enabled.
paint_style.invert();
} else if (n == selected_) {
// Otherwise only highlight the selected symbol.
paint_style.invert();
}
if (editing_ && n == selected_) {
// Use 'bg_blue' style to indicate in editing mode.
paint_style.foreground = Theme::getInstance()->bg_darkest->foreground;
paint_style.background = Theme::getInstance()->fg_blue->foreground;
}
}
painter.draw_char(p, paint_style, c);
p += {8, 0};
}
}
bool SymField::on_key(KeyEvent key) {
// If explicit edits are enabled, only Select is handled when not in edit mode.
if (explicit_edits_ && !editing_) {
switch (key) {
case KeyEvent::Select:
editing_ = true;
set_dirty();
return true;
default:
return false;
}
}
switch (key) {
case KeyEvent::Select:
editing_ = !editing_;
set_dirty();
return true;
case KeyEvent::Left:
if (selected_ > 0) {
selected_--;
set_dirty();
return true;
}
break;
case KeyEvent::Right:
if (selected_ < (value_.length() - 1)) {
selected_++;
set_dirty();
return true;
}
break;
case KeyEvent::Up:
if (editing_) {
on_encoder(1);
return true;
}
break;
case KeyEvent::Down:
if (editing_) {
on_encoder(-1);
return true;
}
break;
default:
break;
}
return false;
}
bool SymField::on_encoder(EncoderEvent delta) {
if (explicit_edits_ && !editing_)
return false;
// TODO: Wrapping or carrying might be nice.
int offset = get_offset(selected_) + delta;
offset = clip<int>(offset, 0, symbols_.length() - 1);
set_offset(selected_, offset);
return true;
}
bool SymField::on_touch(TouchEvent event) {
if (event.type == TouchEvent::Type::Start)
focus();
return true;
}
char SymField::ensure_valid(char symbol) const {
// NB: Linear search - symbol lists are small.
auto pos = symbols_.find(symbol);
return pos != std::string::npos ? symbol : symbols_[0];
}
void SymField::ensure_all_symbols() {
auto temp = value_;
for (auto& c : value_)
c = ensure_valid(c);
if (temp != value_) {
if (on_change)
on_change(*this);
set_dirty();
}
}
void SymField::set_symbol_internal(size_t index, char symbol) {
if (value_[index] == symbol)
return;
value_[index] = symbol;
if (on_change)
on_change(*this);
set_dirty();
}
uint8_t SymField::get_radix() const {
switch (type_) {
case Type::Oct:
return 8;
case Type::Dec:
return 10;
case Type::Hex:
return 16;
default:
return 0;
}
}
/* Waveform **************************************************************/
Waveform::Waveform(
Rect parent_rect,
int16_t* data,
uint32_t length,
uint32_t offset,
bool digital,
Color color,
bool clickable)
: Widget{parent_rect},
data_{data},
length_{length},
offset_{offset},
digital_{digital},
color_{color},
clickable_{clickable} {
if (clickable) {
set_focusable(true);
// previous_data.resize(length_, 0);
}
}
void Waveform::set_cursor(const uint32_t i, const int16_t position) {
if (i < 2) {
if (position != cursors[i]) {
cursors[i] = position;
set_dirty();
}
show_cursors = true;
}
}
void Waveform::set_offset(const uint32_t new_offset) {
if (new_offset != offset_) {
offset_ = new_offset;
set_dirty();
}
}
void Waveform::set_length(const uint32_t new_length) {
if (new_length != length_) {
length_ = new_length;
set_dirty();
}
}
bool Waveform::is_paused() const {
return paused_;
}
void Waveform::set_paused(bool paused) {
paused_ = paused;
if (!paused) {
if_ever_painted_pause = false;
}
set_dirty();
}
bool Waveform::is_clickable() const {
return clickable_;
}
void Waveform::getAccessibilityText(std::string& result) {
result = paused_ ? "paused waveform" : "waveform";
}
void Waveform::getWidgetName(std::string& result) {
result = "Waveform";
}
bool Waveform::on_key(const KeyEvent key) {
if (!clickable_) return false;
if (key == KeyEvent::Select) {
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
}
return false;
}
bool Waveform::on_keyboard(const KeyboardEvent key) {
if (!clickable_) return false;
if (key == 32 || key == 10) {
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
}
return false;
}
bool Waveform::on_touch(const TouchEvent event) {
if (!clickable_) return false;
switch (event.type) {
case TouchEvent::Type::Start:
focus();
return true;
case TouchEvent::Type::End:
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
default:
return false;
}
}
void Waveform::paint(Painter& painter) {
// previously it's upside down , low level is up and high level is down, which doesn't make sense,
// if that was made for a reason, feel free to revert.
if (paused_) {
// TODO: this is bad: that it still enter this func and still consume resources.
// even do a if(paused_) return; comsume too, but not that much.
if (dirty() && !if_ever_painted_pause) {
// clear
painter.fill_rectangle_unrolled8(screen_rect(), Theme::getInstance()->bg_darkest->background);
// draw "WF HIDDEN" text
const auto r = screen_rect();
painter.draw_string(
{r.center().x() - 24, r.center().y() - 8},
style(),
"WF HIDDEN");
if_ever_painted_pause = true;
}
if (show_cursors) {
for (uint32_t n = 0; n < 2; n++) {
painter.draw_vline(
Point(std::min(screen_rect().size().width(), (int)cursors[n]), screen_rect().location().y()),
screen_rect().size().height(),
cursor_colors[n]);
}
}
return;
}
// not paused
size_t n;
Coord y, y_offset = screen_rect().location().y();
Coord prev_x = screen_rect().location().x(), prev_y;
float x, x_inc;
Dim h = screen_rect().size().height();
const float y_scale = (float)(h - 1) / 65536.0;
int16_t* data_start = data_ + offset_;
// Clear
painter.fill_rectangle_unrolled8(screen_rect(), Theme::getInstance()->bg_darkest->background);
if (!length_) return;
x_inc = (float)screen_rect().size().width() / length_;
if (digital_) {
// Digital waveform: each value is an horizontal line
x = 0;
h--;
for (n = 0; n < length_; n++) {
y = *(data_start++) ? 0 : h;
if (n) {
if (y != prev_y)
painter.draw_vline({(Coord)x, y_offset}, h, color_);
}
painter.draw_hline({(Coord)x, y_offset + y}, ceil(x_inc), color_);
prev_y = y;
x += x_inc;
}
} else {
// Analog waveform: each value is a point's Y coordinate
x = prev_x + x_inc;
h /= 2;
prev_y = y_offset + h - (*(data_start++) * y_scale);
for (n = 1; n < length_; n++) {
y = y_offset + h - (*(data_start++) * y_scale);
display.draw_line({prev_x, prev_y}, {(Coord)x, y}, color_);
prev_x = x;
prev_y = y;
x += x_inc;
}
}
// Cursors
if (show_cursors) {
for (n = 0; n < 2; n++) {
painter.draw_vline(
Point(std::min(screen_rect().size().width(), (int)cursors[n]), y_offset),
screen_rect().size().height(),
cursor_colors[n]);
}
}
// focused highlight border
if (clickable_ && has_focus()) {
painter.draw_rectangle(
screen_rect(),
Theme::getInstance()->fg_light->foreground);
}
}
/* GraphEq *************************************************************/
GraphEq::GraphEq(
Rect parent_rect,
bool clickable)
: Widget{parent_rect},
clickable_{clickable},
bar_heights(NUM_BARS, 0),
prev_bar_heights(NUM_BARS, 0) {
if (clickable) {
set_focusable(true);
// previous_data.resize(length_, 0);
}
}
void GraphEq::set_parent_rect(const Rect new_parent_rect) {
Widget::set_parent_rect(new_parent_rect);
calculate_params();
}
void GraphEq::calculate_params() {
y_top = screen_rect().top();
RENDER_HEIGHT = parent_rect().height();
BAR_WIDTH = (parent_rect().width() - (BAR_SPACING * (NUM_BARS - 1))) / NUM_BARS;
HORIZONTAL_OFFSET = screen_rect().left();
}
bool GraphEq::is_paused() const {
return paused_;
}
void GraphEq::set_paused(bool paused) {
paused_ = paused;
needs_background_redraw = true;
set_dirty();
}
bool GraphEq::is_clickable() const {
return clickable_;
}
void GraphEq::getAccessibilityText(std::string& result) {
result = paused_ ? "paused GraphEq" : "GraphEq";
}
void GraphEq::getWidgetName(std::string& result) {
result = "GraphEq";
}
bool GraphEq::on_key(const KeyEvent key) {
if (!clickable_) return false;
if (key == KeyEvent::Select) {
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
}
return false;
}
bool GraphEq::on_keyboard(const KeyboardEvent key) {
if (!clickable_) return false;
if (key == 32 || key == 10) {
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
}
return false;
}
bool GraphEq::on_touch(const TouchEvent event) {
if (!clickable_) return false;
switch (event.type) {
case TouchEvent::Type::Start:
focus();
return true;
case TouchEvent::Type::End:
set_paused(!paused_);
if (on_select) {
on_select(*this);
}
return true;
default:
return false;
}
}
void GraphEq::set_theme(Color base_color_, Color peak_color_) {
base_color = base_color_;
peak_color = peak_color_;
set_dirty();
}
void GraphEq::update_audio_spectrum(const AudioSpectrum& spectrum) {
const float bin_frequency_size = 48000.0f / 128;
for (int bar = 0; bar < NUM_BARS; bar++) {
float start_freq = FREQUENCY_BANDS[bar];
float end_freq = FREQUENCY_BANDS[bar + 1];
int start_bin = std::max(1, (int)(start_freq / bin_frequency_size));
int end_bin = std::min(127, (int)(end_freq / bin_frequency_size));
if (start_bin >= end_bin) {
end_bin = start_bin + 1;
}
float total_energy = 0;
int bin_count = 0;
for (int bin = start_bin; bin <= end_bin; bin++) {
total_energy += spectrum.db[bin];
bin_count++;
}
float avg_db = bin_count > 0 ? (total_energy / bin_count) : 0;
// Manually boost highs for better visual balance
float treble_boost = 1.0f;
if (bar == 10)
treble_boost = 1.7f;
else if (bar >= 9)
treble_boost = 1.3f;
else if (bar >= 7)
treble_boost = 1.3f;
// Mid emphasis for a V-shape effect
float mid_boost = 1.0f;
if (bar == 4 || bar == 5 || bar == 6) mid_boost = 1.2f;
float amplified_db = avg_db * treble_boost * mid_boost;
if (amplified_db > 255) amplified_db = 255;
float band_scale = 1.0f;
int target_height = (amplified_db * RENDER_HEIGHT * band_scale) / 255;
if (target_height > RENDER_HEIGHT) {
target_height = RENDER_HEIGHT;
}
// Adjusted to look nice to my eyes
float rise_speed = 0.8f;
float fall_speed = 1.0f;
if (target_height > bar_heights[bar]) {
bar_heights[bar] = bar_heights[bar] * (1.0f - rise_speed) + target_height * rise_speed;
} else {
bar_heights[bar] = bar_heights[bar] * (1.0f - fall_speed) + target_height * fall_speed;
}
}
set_dirty();
}
void GraphEq::paint(Painter& painter) {
if (!visible()) return;
if (!is_calculated) { // calc positions first
calculate_params();
is_calculated = true;
}
if (needs_background_redraw) {
painter.fill_rectangle(screen_rect(), Theme::getInstance()->bg_darkest->background);
needs_background_redraw = false;
}
if (paused_) {
return;
}
const int num_segments = RENDER_HEIGHT / SEGMENT_HEIGHT;
uint16_t bottom = screen_rect().bottom();
for (int bar = 0; bar < NUM_BARS; bar++) {
int x = HORIZONTAL_OFFSET + bar * (BAR_WIDTH + BAR_SPACING);
int active_segments = (bar_heights[bar] * num_segments) / RENDER_HEIGHT;
if (prev_bar_heights[bar] > active_segments) {
int clear_height = (prev_bar_heights[bar] - active_segments) * SEGMENT_HEIGHT;
int clear_y = bottom - prev_bar_heights[bar] * SEGMENT_HEIGHT;
painter.fill_rectangle({x, clear_y, BAR_WIDTH, clear_height}, Theme::getInstance()->bg_darkest->background);
}
for (int seg = 0; seg < active_segments; seg++) {
int y = bottom - (seg + 1) * SEGMENT_HEIGHT;
if (y < y_top) break;
Color segment_color = (seg >= active_segments - 2 && seg < active_segments) ? peak_color : base_color;
painter.fill_rectangle({x, y, BAR_WIDTH, SEGMENT_HEIGHT - 1}, segment_color);
}
prev_bar_heights[bar] = active_segments;
}
}
/* VuMeter **************************************************************/
VuMeter::VuMeter(
Rect parent_rect,
uint32_t LEDs,
bool show_max)
: Widget{parent_rect},
LEDs_{LEDs},
show_max_{show_max} {
// set_focusable(false);
LED_height = std::max(1UL, parent_rect.size().height() / LEDs);
split = 256 / LEDs;
}
void VuMeter::set_value(const uint32_t new_value) {
if ((new_value != value_) && (new_value < 256)) {
value_ = new_value;
set_dirty();
}
}
void VuMeter::set_mark(const uint32_t new_mark) {
if ((new_mark != mark) && (new_mark < 256)) {
mark = new_mark;
set_dirty();
}
}
void VuMeter::paint(Painter& painter) {
uint32_t bar;
Color color;
bool lit = false;
uint32_t bar_level;
Point pos = screen_rect().location();
Dim width = screen_rect().size().width() - 4;
Dim height = screen_rect().size().height();
Dim bottom = pos.y() + height;
Coord marks_x = pos.x() + width;
if (value_ != prev_value) {
bar_level = LEDs_ - ((value_ + 1) / split);
// Draw LEDs
for (bar = 0; bar < LEDs_; bar++) {
if (bar >= bar_level)
lit = true;
if (bar == 0)
color = lit ? Theme::getInstance()->fg_red->foreground : Theme::getInstance()->bg_dark->background;
else if (bar == 1)
color = lit ? Theme::getInstance()->fg_orange->foreground : Theme::getInstance()->bg_dark->background;
else if ((bar == 2) || (bar == 3))
color = lit ? Theme::getInstance()->fg_yellow->foreground : Theme::getInstance()->bg_dark->background;
else
color = lit ? Theme::getInstance()->fg_green->foreground : Theme::getInstance()->bg_dark->background;
painter.fill_rectangle({pos.x(), pos.y() + (Coord)(bar * (LED_height + 1)), width, (Coord)LED_height}, color);
}
prev_value = value_;
}
// Update max level
if (show_max_) {
if (value_ > max) {
max = value_;
hold_timer = 30; // 0.5s @ 60Hz
} else {
if (hold_timer) {
hold_timer--;
} else {
if (max) max--; // Let it drop
}
}
// Draw max level
if (max != prev_max) {
painter.draw_hline({marks_x, bottom - (height * prev_max) / 256}, 8, Theme::getInstance()->bg_darkest->background);
painter.draw_hline({marks_x, bottom - (height * max) / 256}, 8, Theme::getInstance()->bg_darkest->foreground);
if (prev_max == mark)
prev_mark = 0; // Force mark refresh
prev_max = max;
}
}
// Draw mark (forced refresh)
if (mark) {
painter.draw_hline({marks_x, bottom - (height * prev_mark) / 256}, 8, Theme::getInstance()->bg_darkest->background);
painter.draw_hline({marks_x, bottom - (height * mark) / 256}, 8, Theme::getInstance()->fg_medium->foreground);
prev_mark = mark;
}
}
} /* namespace ui */