Externalize widget (#2688)

This commit is contained in:
Totoo 2025-06-09 12:52:40 +02:00 committed by GitHub
parent 00853f526a
commit be372e12bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 314 additions and 0 deletions

View file

@ -35,6 +35,8 @@ using namespace ui;
namespace ui::external_app::fmradio { namespace ui::external_app::fmradio {
#include "external/ui_grapheq.cpi"
void FmRadioView::focus() { void FmRadioView::focus() {
field_frequency.focus(); field_frequency.focus();
} }

View file

@ -47,6 +47,8 @@ using namespace ui;
namespace ui::external_app::fmradio { namespace ui::external_app::fmradio {
#include "external/ui_grapheq.hpp"
#define FMR_BTNGRID_TOP 60 #define FMR_BTNGRID_TOP 60
class FmRadioView : public View { class FmRadioView : public View {

View file

@ -20,6 +20,8 @@ using namespace portapack;
namespace ui::external_app::gfxeq { namespace ui::external_app::gfxeq {
#include "external/ui_grapheq.cpi"
gfxEQView::gfxEQView(NavigationView& nav) gfxEQView::gfxEQView(NavigationView& nav)
: nav_{nav} { : nav_{nav} {
add_children({&button_frequency, &field_rf_amp, &field_lna, &field_vga, add_children({&button_frequency, &field_rf_amp, &field_lna, &field_vga,

View file

@ -22,6 +22,8 @@
namespace ui::external_app::gfxeq { namespace ui::external_app::gfxeq {
#include "external/ui_grapheq.hpp"
class gfxEQView : public View { class gfxEQView : public View {
public: public:
gfxEQView(NavigationView& nav); gfxEQView(NavigationView& nav);

View file

@ -0,0 +1,40 @@
# External UI elements
## Read carefully
These widgets are only used in external apps!
The concept is, we move these widgets out of FW space, and compile them with external apps. To all separately. This is multiple include, but since we compile the widget under different namespaces (and those namespaces will be under the external_app namespace) these all will be removed from the base firmware.
This way we can free up some spaces, and still reuse the same widgets in different apps easily.
## How to create external widget
You create a **hpp** file indet the ui/external folder, and include everything you need for your widget as you normally wanted to do.
**Don't forget the ifdef guards!**
**Never use any namespace for your class there!** The namespace of the actual ext app will be used where you include this.
Then create a **cpi** file, where you include your hpp file, and create the implementation of your widget class. Still, don't use namespace.
You won't need to include these files in any cmake file! (see usage, why)
## How to use these widgets
This is important, so follow the rules carefully. If it works, doesn't mean, you did it good!
In your external app's hpp file, you need to include the widget's hpp file.
**But be careful**, you must include it under the ext app's namespace, before your first class. For examlpe:
namespace ui::external_app::gfxeq {
#include "external/ui_grapheq.hpp"
class gfxEQView : public View {
Then you must include the cpi file in the external app's cpp file. Again, it must be under the ext app's namespace!
using namespace portapack;
namespace ui::external_app::gfxeq {
#include "external/ui_grapheq.cpi"
gfxEQView::gfxEQView(NavigationView& nav)
This must be done under each ext app that uses the same widget.
This way, the widget will be compiled multiple times under the external_app namespace, but those will be removed from the base. Each app will be able to use it, easy to handle, easy to create.

View file

@ -0,0 +1,200 @@
#include "ui_grapheq.hpp"
/* 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;
}
}

View file

@ -0,0 +1,66 @@
#ifndef __UI_GRAPHEQ_H__
#define __UI_GRAPHEQ_H__
#include "ui_widget.hpp"
class GraphEq : public Widget {
public:
std::function<void(GraphEq&)> on_select{};
GraphEq(Rect parent_rect, bool clickable = false);
GraphEq(const GraphEq&) = delete;
GraphEq(GraphEq&&) = delete;
GraphEq& operator=(const GraphEq&) = delete;
GraphEq& operator=(GraphEq&&) = delete;
bool is_paused() const;
void set_paused(bool paused);
bool is_clickable() const;
void paint(Painter& painter) override;
bool on_key(const KeyEvent key) override;
bool on_touch(const TouchEvent event) override;
bool on_keyboard(const KeyboardEvent event) override;
void set_parent_rect(const Rect new_parent_rect) override;
void getAccessibilityText(std::string& result) override;
void getWidgetName(std::string& result) override;
void update_audio_spectrum(const AudioSpectrum& spectrum);
void set_theme(Color base_color, Color peak_color);
private:
bool is_calculated{false};
bool paused_{false};
bool clickable_{false};
bool needs_background_redraw{true}; // Redraw background only when needed.
Color base_color = Color(255, 0, 255);
Color peak_color = Color(255, 255, 255);
std::vector<ui::Dim> bar_heights;
std::vector<ui::Dim> prev_bar_heights;
ui::Dim y_top = 2 * 16;
ui::Dim RENDER_HEIGHT = 288;
ui::Dim BAR_WIDTH = 20;
ui::Dim HORIZONTAL_OFFSET = 2;
static const int NUM_BARS = 11;
static const int BAR_SPACING = 2;
static const int SEGMENT_HEIGHT = 10;
static constexpr std::array<int16_t, NUM_BARS + 1> FREQUENCY_BANDS = {
375, // Bass warmth and low rumble (e.g., deep basslines, kick drum body)
750, // Upper bass punch (e.g., bass guitar punch, kick drum attack)
1500, // Lower midrange fullness (e.g., warmth in vocals, guitar body)
2250, // Midrange clarity (e.g., vocal presence, snare crack)
3375, // Upper midrange bite (e.g., instrument definition, vocal articulation)
4875, // Presence and edge (e.g., guitar bite, vocal sibilance start)
6750, // Lower brilliance (e.g., cymbal shimmer, vocal clarity)
9375, // Brilliance and air (e.g., hi-hat crispness, breathy vocals)
13125, // High treble sparkle (e.g., subtle overtones, synth shimmer)
16875, // Upper treble airiness (e.g., faint harmonics, room ambiance)
20625, // Top-end sheen (e.g., ultra-high harmonics, noise floor)
24375 // Extreme treble limit (e.g., inaudible overtones, signal cutoff, static)
};
void calculate_params(); // re calculate some parameters based on parent_rect()
};
#endif /*__UI_GRAPHEQ_H__*/