feat(ui): add app launcher and NomadNet MVP

This commit is contained in:
torlando-agent[bot]
2026-08-05 00:22:59 +00:00
parent bf7f326d15
commit d0e1ce273c
26 changed files with 2464 additions and 247 deletions
+140
View File
@@ -0,0 +1,140 @@
#include "NomadNetScreen.h"
#ifdef ARDUINO
#include "Theme.h"
#include "../LVGL/LVGLInit.h"
#include "../TextAreaHelper.h"
#include <algorithm>
namespace UI::LXMF {
NomadNetScreen::NomadNetScreen() {
_screen=lv_obj_create(lv_scr_act()); lv_obj_set_size(_screen,320,240);
lv_obj_set_style_bg_color(_screen,Theme::surface(),0); lv_obj_set_style_border_width(_screen,0,0); lv_obj_set_style_pad_all(_screen,0,0);
lv_obj_t* header=lv_obj_create(_screen); lv_obj_set_size(header,LV_PCT(100),34); lv_obj_align(header,LV_ALIGN_TOP_MID,0,0);
lv_obj_set_style_bg_color(header,Theme::surfaceHeader(),0); lv_obj_set_style_border_width(header,0,0); lv_obj_set_style_pad_all(header,2,0);
_back_button=lv_btn_create(header); lv_obj_set_size(_back_button,38,28); lv_obj_align(_back_button,LV_ALIGN_LEFT_MID,0,0);
lv_obj_t* bl=lv_label_create(_back_button);lv_label_set_text(bl,LV_SYMBOL_LEFT);lv_obj_center(bl);
_home_button=lv_btn_create(header);lv_obj_set_size(_home_button,38,28);lv_obj_align(_home_button,LV_ALIGN_LEFT_MID,42,0);
lv_obj_t* hl=lv_label_create(_home_button);lv_label_set_text(hl,LV_SYMBOL_HOME);lv_obj_center(hl);
lv_obj_t* title=lv_label_create(header);lv_label_set_text(title,"NomadNet");lv_obj_center(title);
_reload_button=lv_btn_create(header);lv_obj_set_size(_reload_button,42,28);lv_obj_align(_reload_button,LV_ALIGN_RIGHT_MID,0,0);
lv_obj_t* rl=lv_label_create(_reload_button);lv_label_set_text(rl,LV_SYMBOL_REFRESH);lv_obj_center(rl);
lv_obj_t* address_row=lv_obj_create(_screen);lv_obj_set_size(address_row,LV_PCT(100),38);lv_obj_align(address_row,LV_ALIGN_TOP_MID,0,34);
lv_obj_set_style_bg_color(address_row,Theme::surface(),0);lv_obj_set_style_border_width(address_row,0,0);lv_obj_set_style_pad_all(address_row,3,0);
_address=lv_textarea_create(address_row);lv_obj_set_size(_address,270,30);lv_obj_align(_address,LV_ALIGN_LEFT_MID,0,0);
lv_textarea_set_one_line(_address,true);lv_textarea_set_max_length(_address,511);lv_textarea_set_placeholder_text(_address,"32-hex-destination:/page/path");
lv_obj_set_style_text_font(_address,&lv_font_montserrat_12,0);TextAreaHelper::enable_paste(_address);
_go_button=lv_btn_create(address_row);lv_obj_set_size(_go_button,42,30);lv_obj_align(_go_button,LV_ALIGN_RIGHT_MID,0,0);
lv_obj_t* gl=lv_label_create(_go_button);lv_label_set_text(gl,"Go");lv_obj_center(gl);
_status=lv_label_create(_screen);lv_obj_set_size(_status,312,18);lv_obj_align(_status,LV_ALIGN_TOP_LEFT,4,72);
lv_obj_set_style_text_font(_status,&lv_font_montserrat_12,0);lv_obj_set_style_text_color(_status,Theme::textTertiary(),0);
_content=lv_obj_create(_screen);lv_obj_set_size(_content,320,150);lv_obj_align(_content,LV_ALIGN_BOTTOM_MID,0,0);
lv_obj_set_style_bg_color(_content,Theme::surface(),0);lv_obj_set_style_border_width(_content,0,0);lv_obj_set_style_pad_all(_content,6,0);
lv_obj_set_flex_flow(_content,LV_FLEX_FLOW_COLUMN);lv_obj_set_flex_align(_content,LV_FLEX_ALIGN_START,LV_FLEX_ALIGN_START,LV_FLEX_ALIGN_START);
lv_obj_set_scroll_dir(_content,LV_DIR_VER);lv_obj_set_scrollbar_mode(_content,LV_SCROLLBAR_MODE_AUTO);
for(auto* o:{_back_button,_home_button,_reload_button,_go_button})lv_obj_add_event_cb(o,clicked,LV_EVENT_CLICKED,this);
lv_obj_add_event_cb(_address,clicked,LV_EVENT_READY,this);
set_status("Enter a NomadNet address");hide();
}
NomadNetScreen::~NomadNetScreen(){if(_screen)lv_obj_del(_screen);}
void NomadNetScreen::set_address(const std::string& value){lv_textarea_set_text(_address,value.c_str());}
std::string NomadNetScreen::address()const{return lv_textarea_get_text(_address);}
void NomadNetScreen::set_status(const char* value){lv_label_set_text(_status,value?value:"");}
void NomadNetScreen::set_page(const NomadNet::Document& document) {
auto* group = LVGL::LVGLInit::get_default_group();
if (group) for (auto* object : _focusables) lv_group_remove_obj(object);
lv_obj_clean(_content);
_focusables.clear();
_link_targets.clear();
std::size_t objects = 0;
std::size_t spans = 0;
bool render_truncated = false;
lv_obj_set_style_bg_color(_content, document.has_background
? lv_color_hex(document.background) : Theme::surface(), 0);
for (const auto& block : document.blocks) {
if (objects >= MAX_UI_OBJECTS || spans >= MAX_UI_SPANS) {
render_truncated = true;
break;
}
if (block.type == NomadNet::BlockType::DIVIDER) {
lv_obj_t* line = lv_obj_create(_content);
lv_obj_set_size(line, 304, 1);
lv_obj_set_style_bg_color(line, Theme::border(), 0);
lv_obj_set_style_border_width(line, 0, 0);
++objects;
continue;
}
// Spans preserve mixed inline styling and wrap the complete source line
// together instead of introducing breaks at every style transition.
lv_obj_t* text = lv_spangroup_create(_content);
const lv_coord_t indent = block.type == NomadNet::BlockType::HEADING
? 0 : static_cast<lv_coord_t>(std::min<unsigned>(block.depth, 8) * 4);
lv_obj_set_width(text, 304 - indent);
lv_obj_set_style_translate_x(text, indent, 0);
lv_spangroup_set_mode(text, LV_SPAN_MODE_BREAK);
lv_spangroup_set_overflow(text, LV_SPAN_OVERFLOW_CLIP);
lv_spangroup_set_lines(text, -1);
lv_spangroup_set_align(text, block.alignment == NomadNet::Alignment::CENTER
? LV_TEXT_ALIGN_CENTER : block.alignment == NomadNet::Alignment::RIGHT
? LV_TEXT_ALIGN_RIGHT : LV_TEXT_ALIGN_LEFT);
++objects;
for (const auto& run : block.runs) {
if (spans >= MAX_UI_SPANS) { render_truncated = true; break; }
lv_span_t* span = lv_spangroup_new_span(text);
lv_span_set_text(span, run.text.c_str());
const bool is_link = run.link_index >= 0 &&
static_cast<std::size_t>(run.link_index) < document.links.size();
lv_style_set_text_color(&span->style, is_link ? Theme::primaryLight() :
run.has_foreground ? lv_color_hex(run.foreground) :
document.has_foreground ? lv_color_hex(document.foreground) : Theme::textPrimary());
lv_style_set_text_font(&span->style,
(run.bold || block.type == NomadNet::BlockType::HEADING)
? &lv_font_montserrat_16 : &lv_font_montserrat_12);
lv_style_set_text_line_space(&span->style, 3);
if (run.underline || is_link)
lv_style_set_text_decor(&span->style, LV_TEXT_DECOR_UNDERLINE);
if (run.italic) lv_style_set_text_letter_space(&span->style, 1);
if (run.has_background) lv_style_set_bg_color(&span->style, lv_color_hex(run.background));
++spans;
}
lv_spangroup_refr_mode(text);
// A visual span cannot itself participate in the encoder focus group.
// Add one bounded, focusable activation control for every rendered link.
for (const auto& run : block.runs) {
if (run.link_index < 0 || static_cast<std::size_t>(run.link_index) >= document.links.size()) continue;
if (objects + 2 > MAX_UI_OBJECTS) { render_truncated = true; break; }
lv_obj_t* button = lv_btn_create(_content);
lv_obj_set_size(button, 304, 28);
lv_obj_set_style_pad_all(button, 4, 0);
lv_obj_t* label = lv_label_create(button);
const std::string caption = "Open: " + run.text;
lv_label_set_text(label, caption.c_str());
lv_label_set_long_mode(label, LV_LABEL_LONG_DOT);
lv_obj_set_width(label, 294);
lv_obj_center(label);
_link_targets.push_back(document.links[run.link_index].target);
lv_obj_set_user_data(button, reinterpret_cast<void*>(_link_targets.size()));
lv_obj_add_event_cb(button, clicked, LV_EVENT_CLICKED, this);
_focusables.push_back(button);
if (_visible && group) lv_group_add_obj(group, button);
objects += 2;
}
}
if (document.truncated || render_truncated) {
lv_obj_t* notice = lv_label_create(_content);
lv_label_set_text(notice, "[Page truncated to device safety limits]");
lv_obj_set_style_text_color(notice, Theme::warning(), 0);
} else if (document.unsupported) {
lv_obj_t* notice = lv_label_create(_content);
lv_label_set_text(notice, "Forms, partials, tables and downloads are unsupported in this MVP.");
lv_obj_set_style_text_color(notice, Theme::warning(), 0);
}
}
void NomadNetScreen::show(){_visible=true;lv_obj_clear_flag(_screen,LV_OBJ_FLAG_HIDDEN);lv_obj_move_foreground(_screen);auto*g=LVGL::LVGLInit::get_default_group();if(g){for(auto*o:{_back_button,_home_button,_reload_button,_address,_go_button})lv_group_add_obj(g,o);for(auto*o:_focusables)lv_group_add_obj(g,o);lv_group_focus_obj(_address);}}
void NomadNetScreen::hide(){_visible=false;auto*g=LVGL::LVGLInit::get_default_group();if(g){for(auto*o:{_back_button,_home_button,_reload_button,_address,_go_button})lv_group_remove_obj(o);for(auto*o:_focusables)lv_group_remove_obj(o);}lv_obj_add_flag(_screen,LV_OBJ_FLAG_HIDDEN);}
void NomadNetScreen::clicked(lv_event_t*e){auto*s=static_cast<NomadNetScreen*>(lv_event_get_user_data(e));auto*t=lv_event_get_target(e);if(t==s->_back_button&&s->_back)s->_back();else if(t==s->_home_button&&s->_home)s->_home();else if(t==s->_reload_button&&s->_reload)s->_reload();else if((t==s->_go_button||t==s->_address)&&s->_open)s->_open(s->address());else{std::size_t n=reinterpret_cast<std::size_t>(lv_obj_get_user_data(t));if(n>0&&n<=s->_link_targets.size()&&s->_link)s->_link(s->_link_targets[n-1]);}}
}
#endif