/* * 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 #include #include #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::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 drawn status. dirty_overlapping_children_in_rect(screen_rect()); drawn(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::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 { for (const Widget* curr = this; curr; curr = curr->parent()) { if (curr->style_) return *curr->style_; } // default style should always be set by SystemView return *style_; } void Widget::drawn(bool v) { if (v != flags.drawn) { flags.drawn = 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 not drawn too. for (const auto child : children()) { child->drawn(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 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()); widget->set_parent(nullptr); } } void View::remove_children(const std::vector& children) { for (auto child : children) { remove_child(child); } } const std::vector& 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(); } std::string Text::get() { return text; } 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 default_style = has_focus() ? style().invert() : style(); painter.fill_rectangle(rect, default_style.background); const int char_width = default_style.font.char_width(); const int line_height = default_style.font.line_height(); if (char_width == 0 || line_height == 0) return; const size_t chars_per_line = rect.width() / char_width; if (chars_per_line == 0) return; size_t lines_capacity = rect.height() / line_height; if (lines_capacity == 0) lines_capacity = 1; auto text_view = std::string_view{text}; size_t current_offset = 0; // Track the active text color across line breaks Color active_color = default_style.foreground; for (size_t line_idx = 0; line_idx < lines_capacity; ++line_idx) { if (current_offset >= text_view.length()) break; size_t scan_offset = current_offset; size_t printable_count = 0; Color next_color = active_color; // This will hold the color state as we scan the current chunk // Scan forward to find chunk length AND track color changes while (scan_offset < text_view.length() && printable_count < chars_per_line) { if (text_view[scan_offset] == '\x1B') { scan_offset++; if (scan_offset < text_view.length()) { uint8_t color_idx = text_view[scan_offset]; // Mirror the logic from Painter::draw_string if (color_idx < std::size(term_colors)) { next_color = term_colors[color_idx]; } else { next_color = active_color; } scan_offset++; } } else { scan_offset++; printable_count++; } } size_t chunk_len = scan_offset - current_offset; // Create a temporary style for this line, initialized with the carried-over color Style line_style = {.font = default_style.font, .background = default_style.background, .foreground = active_color}; painter.draw_string( rect.location() + Point(0, line_idx * line_height), line_style, text_view.substr(current_offset, chunk_len)); current_offset += chunk_len; // Carry the final color state of this line into the start of the next line active_color = next_color; } } bool Text::on_touch(const TouchEvent event) { if (event.type == TouchEvent::Type::Start) { if (on_select) { on_select(*this); return true; } } return false; } /* Labels ****************************************************************/ Labels::Labels( std::initializer_list