mayhem-firmware/firmware/common/ui_widget.cpp

723 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) {
painter.draw_string(
screen_pos(),
style(),
text
);
}
/* 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::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 */