mirror of
https://github.com/eried/portapack-mayhem.git
synced 2024-10-01 01:26:06 -04:00
737 lines
14 KiB
C++
737 lines
14 KiB
C++
/*
|
|
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
|
*
|
|
* 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>
|
|
|
|
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;
|
|
}
|
|
|
|
constexpr size_t to_string_max_length = 16;
|
|
|
|
static char* to_string_dec_uint_internal(
|
|
char* p,
|
|
uint32_t n
|
|
) {
|
|
*p = 0;
|
|
auto q = p;
|
|
|
|
do {
|
|
const uint32_t d = n % 10;
|
|
const char c = d + 48;
|
|
*(--q) = c;
|
|
n /= 10;
|
|
} while( n != 0 );
|
|
|
|
return q;
|
|
}
|
|
|
|
static char* to_string_dec_uint_pad_internal(
|
|
char* const term,
|
|
const uint32_t n,
|
|
const int32_t l,
|
|
const char fill
|
|
) {
|
|
auto q = to_string_dec_uint_internal(term, n);
|
|
|
|
if( fill ) {
|
|
while( (term - q) < l ) {
|
|
*(--q) = fill;
|
|
}
|
|
}
|
|
|
|
return q;
|
|
}
|
|
|
|
std::string to_string_dec_uint(
|
|
const uint32_t n,
|
|
const int32_t l,
|
|
const char fill
|
|
) {
|
|
char p[16];
|
|
auto term = p + sizeof(p) - 1;
|
|
auto q = to_string_dec_uint_pad_internal(term, n, l, fill);
|
|
|
|
// Right justify.
|
|
while( (term - q) < l ) {
|
|
*(--q) = ' ';
|
|
}
|
|
|
|
return q;
|
|
}
|
|
|
|
std::string to_string_dec_int(
|
|
const int32_t n,
|
|
const int32_t l,
|
|
const char fill
|
|
) {
|
|
const size_t negative = (n < 0) ? 1 : 0;
|
|
uint32_t n_abs = negative ? -n : n;
|
|
|
|
char p[16];
|
|
auto term = p + sizeof(p) - 1;
|
|
auto q = to_string_dec_uint_pad_internal(term, n_abs, l - negative, fill);
|
|
|
|
// Add sign.
|
|
if( negative ) {
|
|
*(--q) = '-';
|
|
}
|
|
|
|
// Right justify.
|
|
while( (term - q) < l ) {
|
|
*(--q) = ' ';
|
|
}
|
|
|
|
return q;
|
|
}
|
|
|
|
static void to_string_hex_internal(char* p, const uint32_t n, const int32_t l) {
|
|
const uint32_t d = n & 0xf;
|
|
p[l] = (d > 9) ? (d + 87) : (d + 48);
|
|
if( l > 0 ) {
|
|
to_string_hex_internal(p, n >> 4, l - 1);
|
|
}
|
|
}
|
|
|
|
std::string to_string_hex(const uint32_t n, const int32_t l) {
|
|
char p[16];
|
|
to_string_hex_internal(p, n, l - 1);
|
|
p[l] = 0;
|
|
return p;
|
|
}
|
|
|
|
/* Widget ****************************************************************/
|
|
|
|
Point Widget::screen_pos() {
|
|
return parent() ? (parent()->screen_pos() + parent_rect.pos) : parent_rect.pos;
|
|
}
|
|
|
|
Size Widget::size() const {
|
|
return parent_rect.size;
|
|
}
|
|
|
|
Rect Widget::screen_rect() {
|
|
return parent() ? (parent_rect + parent()->screen_pos()) : 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( parent_ && !widget ) {
|
|
// We have a parent, but are losing it. Update visible status.
|
|
visible(false);
|
|
}
|
|
|
|
parent_ = widget;
|
|
}
|
|
|
|
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 ) {
|
|
parent()->set_dirty();
|
|
/* 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() {
|
|
//set_dirty();
|
|
}
|
|
|
|
void Widget::blur() {
|
|
context().focus_manager().set_focus_widget(nullptr);
|
|
}
|
|
|
|
void Widget::on_blur() {
|
|
//set_dirty();
|
|
}
|
|
|
|
bool Widget::focusable() const {
|
|
return flags.focusable;
|
|
}
|
|
|
|
bool Widget::has_focus() {
|
|
return (context().focus_manager().focus_widget() == this);
|
|
}
|
|
|
|
Widget* Widget::last_child_focus() const {
|
|
return nullptr;
|
|
}
|
|
|
|
void Widget::set_last_child_focus(Widget* const child) {
|
|
// Ignore.
|
|
(void)child;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const std::vector<Widget*> Widget::children() const {
|
|
return { };
|
|
}
|
|
|
|
Context& Widget::context() const {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* View ******************************************************************/
|
|
|
|
void View::set_parent_rect(const Rect new_parent_rect) {
|
|
Widget::set_parent_rect(new_parent_rect);
|
|
dirty_screen_rect += screen_rect();
|
|
}
|
|
|
|
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);
|
|
widget->set_dirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
void View::add_children(const std::vector<Widget*>& children) {
|
|
for(auto child : children) {
|
|
add_child(child);
|
|
}
|
|
}
|
|
|
|
void View::remove_child(Widget* const widget) {
|
|
if( widget ) {
|
|
children_.erase(std::remove(children_.begin(), children_.end(), widget), children_.end());
|
|
dirty_screen_rect += widget->screen_rect();
|
|
widget->set_parent(nullptr);
|
|
if( dirty_screen_rect ) {
|
|
set_dirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
const std::vector<Widget*> View::children() const {
|
|
return children_;
|
|
}
|
|
|
|
Widget* View::initial_focus() {
|
|
return nullptr;
|
|
}
|
|
|
|
/* Rectangle *************************************************************/
|
|
|
|
void Rectangle::set_color(const Color c) {
|
|
color = c;
|
|
set_dirty();
|
|
}
|
|
|
|
void Rectangle::paint(Painter& painter) {
|
|
painter.fill_rectangle(
|
|
screen_rect(),
|
|
color
|
|
);
|
|
}
|
|
|
|
/* Text ******************************************************************/
|
|
|
|
void Text::set(const std::string value) {
|
|
text = value;
|
|
set_dirty();
|
|
}
|
|
|
|
void Text::paint(Painter& painter) {
|
|
if (style_ == nullptr) style_ = &style();
|
|
|
|
painter.draw_string(
|
|
screen_pos(),
|
|
(*style_),
|
|
text
|
|
);
|
|
}
|
|
|
|
void Text::set_style(const Style* new_style) {
|
|
if( new_style != style_ ) {
|
|
style_ = new_style;
|
|
set_dirty();
|
|
}
|
|
}
|
|
|
|
/* Checkbox **************************************************************/
|
|
|
|
void Checkbox::set_text(const std::string value) {
|
|
text_ = value;
|
|
set_dirty();
|
|
}
|
|
|
|
std::string Checkbox::text() const {
|
|
return text_;
|
|
}
|
|
|
|
void Checkbox::set_value(const bool value) {
|
|
value_ = value;
|
|
set_dirty();
|
|
}
|
|
|
|
bool Checkbox::value() const {
|
|
return value_;
|
|
}
|
|
|
|
void Checkbox::paint(Painter& painter) {
|
|
const auto r = screen_rect();
|
|
|
|
const auto paint_style = (has_focus() || flags.highlighted) ? style().invert() : style();
|
|
|
|
painter.draw_rectangle({ r.pos.x, r.pos.y, 24, 24 }, style().foreground);
|
|
|
|
painter.fill_rectangle(
|
|
{
|
|
static_cast<Coord>(r.pos.x + 1), static_cast<Coord>(r.pos.y + 1),
|
|
static_cast<Dim>(24 - 2), static_cast<Dim>(24 - 2)
|
|
},
|
|
style().background
|
|
);
|
|
|
|
painter.draw_rectangle({ r.pos.x+2, r.pos.y+2, 24-4, 24-4 }, paint_style.background);
|
|
|
|
if (value_ == true) {
|
|
// Check
|
|
portapack::display.draw_line( {r.pos.x+2, r.pos.y+14}, {r.pos.x+6, r.pos.y+18}, ui::Color::green());
|
|
portapack::display.draw_line( {r.pos.x+6, r.pos.y+18}, {r.pos.x+20, r.pos.y+4}, ui::Color::green());
|
|
} else {
|
|
// Cross
|
|
portapack::display.draw_line( {r.pos.x+1, r.pos.y+1}, {r.pos.x+24-2, r.pos.y+24-2}, ui::Color::red());
|
|
portapack::display.draw_line( {r.pos.x+24-2, r.pos.y+1}, {r.pos.x+1, r.pos.y+24-2}, ui::Color::red());
|
|
}
|
|
|
|
const auto label_r = paint_style.font.size_of(text_);
|
|
painter.draw_string(
|
|
{
|
|
static_cast<Coord>(r.pos.x + 24 + 4),
|
|
static_cast<Coord>(r.pos.y + (24 - label_r.h) / 2)
|
|
},
|
|
paint_style,
|
|
text_
|
|
);
|
|
}
|
|
|
|
bool Checkbox::on_key(const KeyEvent key) {
|
|
if( key == KeyEvent::Select ) {
|
|
value_ = not value_;
|
|
set_dirty();
|
|
|
|
if( on_select ) {
|
|
on_select(*this);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Checkbox::on_touch(const TouchEvent event) {
|
|
switch(event.type) {
|
|
case TouchEvent::Type::Start:
|
|
flags.highlighted = true;
|
|
set_dirty();
|
|
return true;
|
|
|
|
|
|
case TouchEvent::Type::End:
|
|
flags.highlighted = false;
|
|
value_ = not value_;
|
|
set_dirty();
|
|
if( on_select ) {
|
|
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
|
|
}
|
|
|
|
/* Button ****************************************************************/
|
|
|
|
void Button::set_text(const std::string value) {
|
|
text_ = value;
|
|
set_dirty();
|
|
}
|
|
|
|
void Button::set_text(const int value) { //std::string
|
|
text_ = value;
|
|
set_dirty();
|
|
}
|
|
|
|
void Button::set_style(const Style* new_style) {
|
|
if( new_style != style_ ) {
|
|
style_ = new_style;
|
|
set_dirty();
|
|
}
|
|
}
|
|
|
|
std::string Button::text() const {
|
|
return text_;
|
|
}
|
|
|
|
void Button::paint(Painter& painter) {
|
|
const auto r = screen_rect();
|
|
|
|
if (style_ == nullptr) style_ = &style();
|
|
|
|
const auto paint_style = (has_focus() || flags.highlighted) ? style_->invert() : *(style_);
|
|
|
|
painter.draw_rectangle(r, style().foreground);
|
|
|
|
painter.fill_rectangle(
|
|
{
|
|
static_cast<Coord>(r.pos.x + 1), static_cast<Coord>(r.pos.y + 1),
|
|
static_cast<Dim>(r.size.w - 2), static_cast<Dim>(r.size.h - 2)
|
|
},
|
|
paint_style.background
|
|
);
|
|
|
|
const auto label_r = paint_style.font.size_of(text_);
|
|
painter.draw_string(
|
|
{
|
|
static_cast<Coord>(r.pos.x + (r.size.w - label_r.w) / 2),
|
|
static_cast<Coord>(r.pos.y + (r.size.h - label_r.h) / 2)
|
|
},
|
|
paint_style,
|
|
text_
|
|
);
|
|
}
|
|
|
|
bool Button::on_key(const KeyEvent key) {
|
|
if( key == KeyEvent::Select ) {
|
|
if( on_select ) {
|
|
on_select(*this);
|
|
return true;
|
|
}
|
|
} else {
|
|
if( on_dir ) {
|
|
on_dir(*this, key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Button::on_touch(const TouchEvent event) {
|
|
switch(event.type) {
|
|
case TouchEvent::Type::Start:
|
|
flags.highlighted = true;
|
|
set_dirty();
|
|
return true;
|
|
|
|
|
|
case TouchEvent::Type::End:
|
|
flags.highlighted = false;
|
|
set_dirty();
|
|
if( on_select ) {
|
|
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
|
|
}
|
|
|
|
/* OptionsField **********************************************************/
|
|
|
|
size_t OptionsField::selected_index() const {
|
|
return selected_index_;
|
|
}
|
|
|
|
void OptionsField::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 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);
|
|
break;
|
|
}
|
|
new_index++;
|
|
}
|
|
}
|
|
|
|
void OptionsField::set_options(options_t new_options) {
|
|
options = new_options;
|
|
set_by_value(0);
|
|
set_dirty();
|
|
}
|
|
|
|
void OptionsField::paint(Painter& painter) {
|
|
const auto paint_style = has_focus() ? style().invert() : style();
|
|
|
|
if( selected_index() < options.size() ) {
|
|
const auto text = options[selected_index()].first;
|
|
painter.draw_string(
|
|
screen_pos(),
|
|
paint_style,
|
|
text
|
|
);
|
|
}
|
|
}
|
|
|
|
bool OptionsField::on_encoder(const EncoderEvent delta) {
|
|
set_selected_index(selected_index() + delta);
|
|
return true;
|
|
}
|
|
|
|
bool OptionsField::on_touch(const TouchEvent event) {
|
|
if( event.type == TouchEvent::Type::Start ) {
|
|
focus();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* NumberField ***********************************************************/
|
|
|
|
int32_t NumberField::value() const {
|
|
return value_;
|
|
}
|
|
|
|
void NumberField::set_value(int32_t new_value) {
|
|
new_value = clip_value(new_value);
|
|
|
|
if( new_value != value() ) {
|
|
value_ = new_value;
|
|
if( on_change ) {
|
|
on_change(value_);
|
|
}
|
|
set_dirty();
|
|
}
|
|
}
|
|
|
|
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_encoder(const EncoderEvent delta) {
|
|
set_value(value() + (delta * step));
|
|
return true;
|
|
}
|
|
|
|
bool NumberField::on_touch(const TouchEvent event) {
|
|
if( event.type == TouchEvent::Type::Start ) {
|
|
focus();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int32_t NumberField::clip_value(int32_t value) {
|
|
if( value > range.second ) {
|
|
value = range.second;
|
|
}
|
|
if( value < range.first ) {
|
|
value = range.first;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
} /* namespace ui */
|