mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-10-22 22:11:26 +00:00
Merge pull request #77 from torlando-tech/fix/nomadnet-virtual-layout
Virtualize NomadNet page layout
This commit is contained in:
7 files changed
+352
-103
No files matched your search
@@ -82,8 +82,10 @@ public:
|
||||
}
|
||||
const ExternalVector<uint8_t>& bytes() const { return _bytes; }
|
||||
std::size_t size() const { return _bytes.size(); }
|
||||
std::size_t capacity() const { return _bytes.capacity(); }
|
||||
bool truncated() const { return false; }
|
||||
void clear() { _bytes.clear(); }
|
||||
void release() { ExternalVector<uint8_t>().swap(_bytes); }
|
||||
|
||||
private:
|
||||
ExternalVector<uint8_t> _bytes;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <new>
|
||||
#if defined(PYXIS_NOMAD_MEMORY_DIAGNOSTIC)
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
@@ -133,7 +134,15 @@ void NomadNetScreen::clear_document(){
|
||||
}
|
||||
_page.clear();
|
||||
NomadNet::ExternalVector<LayoutFragment>().swap(_page_layout);
|
||||
NomadNet::ExternalVector<LayoutFragment>().swap(_line_layout);
|
||||
NomadNet::ExternalVector<LayoutCheckpoint>().swap(_layout_checkpoints);
|
||||
NomadNet::ExternalVector<int32_t>().swap(_link_y);
|
||||
NomadNet::ExternalVector<int32_t>().swap(_link_bottom);
|
||||
_page_height=0;
|
||||
_physical_extent=0;
|
||||
_logical_scroll=0;
|
||||
_layout_window_top=0;
|
||||
_layout_window_bottom=0;
|
||||
_selected_link=-1;
|
||||
lv_obj_refresh_self_size(_content);
|
||||
lv_obj_invalidate(_content);
|
||||
@@ -335,48 +344,119 @@ bool NomadNetScreen::set_page(const NomadNet::Document& document) {
|
||||
set_status("Page is too large for available memory");
|
||||
return false;
|
||||
}
|
||||
if(_page.truncated()){
|
||||
const std::string notice=NomadNet::truncation_notice(document);
|
||||
if(!_page.append_notice(notice)){
|
||||
try {
|
||||
if(_page.truncated()){
|
||||
const std::string notice=NomadNet::truncation_notice(document);
|
||||
if(!_page.append_notice(notice)){
|
||||
clear_document();
|
||||
set_status("Page truncation notice could not be retained");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if(_page.unsupported()&&!_page.append_notice("[Unsupported Micron content]")){
|
||||
clear_document();
|
||||
set_status("Page truncation notice could not be retained");
|
||||
set_status("Unsupported-content notice could not be retained");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if(_page.unsupported())_page.append_notice("[Unsupported Micron content]");
|
||||
if(!layout_page()){
|
||||
show_browser(false);
|
||||
if(!layout_page()){
|
||||
clear_document();
|
||||
set_status("Page viewport could not be retained");
|
||||
return false;
|
||||
}
|
||||
lv_obj_set_style_bg_color(_content,_page.has_background()
|
||||
?lv_color_hex(_page.background()):Theme::surface(),0);
|
||||
_page_loaded=true;
|
||||
lv_obj_scroll_to_y(_content,0,LV_ANIM_OFF);
|
||||
lv_obj_refresh_self_size(_content);
|
||||
lv_obj_invalidate(_content);
|
||||
return true;
|
||||
}catch(const std::bad_alloc&){
|
||||
clear_document();
|
||||
set_status("Page layout notice could not be retained");
|
||||
set_status("Page is too large for available memory");
|
||||
return false;
|
||||
}
|
||||
lv_obj_set_style_bg_color(_content,_page.has_background()
|
||||
?lv_color_hex(_page.background()):Theme::surface(),0);
|
||||
_page_loaded=true;
|
||||
show_browser(false);
|
||||
lv_obj_scroll_to_y(_content,0,LV_ANIM_OFF);
|
||||
lv_obj_refresh_self_size(_content);
|
||||
lv_obj_invalidate(_content);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NomadNetScreen::layout_page(){
|
||||
_page_layout.clear();
|
||||
_page_layout.reserve(std::min<std::size_t>(_page.runs().size()*4,MAX_LAYOUT_FRAGMENTS));
|
||||
constexpr std::size_t content_fragment_limit=MAX_LAYOUT_FRAGMENTS-1;
|
||||
_line_layout.clear();
|
||||
_layout_checkpoints.clear();
|
||||
_link_y.assign(_page.links().size(),-1);
|
||||
_link_bottom.assign(_page.links().size(),-1);
|
||||
_page_layout.reserve(MAX_WINDOW_FRAGMENTS);
|
||||
_line_layout.reserve(NomadNet::DocumentParser::MAX_RUNS_PER_LINE);
|
||||
_layout_checkpoints.reserve(_page.blocks().size());
|
||||
const int32_t viewport=std::max<int32_t>(1,lv_obj_get_content_height(_content));
|
||||
if(!layout_from(0,0,0,viewport*3,true))return false;
|
||||
_physical_extent=std::min<int32_t>(_page_height,MAX_PHYSICAL_SCROLL_EXTENT);
|
||||
_logical_scroll=0;
|
||||
_layout_window_top=0;
|
||||
_layout_window_bottom=std::min<int32_t>(_page_height,viewport*3);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NomadNetScreen::append_line_fragment(const LayoutFragment& fragment){
|
||||
if(!_line_layout.empty()){
|
||||
auto& previous=_line_layout.back();
|
||||
const uint32_t combined=static_cast<uint32_t>(previous.byte_length)+fragment.byte_length;
|
||||
if(previous.link_index==fragment.link_index&&previous.large_font==fragment.large_font&&
|
||||
combined<256&&NomadNet::VirtualViewport::can_coalesce(previous.run_index,
|
||||
previous.byte_offset,previous.byte_length,fragment.run_index,fragment.byte_offset)){
|
||||
previous.byte_length=static_cast<uint16_t>(combined);
|
||||
previous.width=static_cast<int16_t>(previous.width+fragment.width);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(_line_layout.size()>=NomadNet::DocumentParser::MAX_RUNS_PER_LINE)return false;
|
||||
_line_layout.push_back(fragment);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NomadNetScreen::commit_line(int32_t line_y,int16_t line_height,
|
||||
NomadNet::Alignment alignment,int16_t indent,
|
||||
int16_t available,int32_t window_top,
|
||||
int32_t window_bottom){
|
||||
if(_line_layout.empty())return true;
|
||||
int16_t line_width=0;
|
||||
for(const auto& fragment:_line_layout)
|
||||
line_width=std::max<int16_t>(line_width,fragment.x+fragment.width-indent);
|
||||
const int16_t shift=alignment==NomadNet::Alignment::CENTER?(available-line_width)/2:
|
||||
alignment==NomadNet::Alignment::RIGHT?available-line_width:0;
|
||||
if(line_y+line_height>=window_top&&line_y<window_bottom){
|
||||
for(auto fragment:_line_layout){
|
||||
if(_page_layout.size()>=MAX_WINDOW_FRAGMENTS)return false;
|
||||
if(shift>0)fragment.x=static_cast<int16_t>(fragment.x+shift);
|
||||
const int32_t relative_y=line_y-window_top;
|
||||
if(relative_y<INT16_MIN||relative_y>INT16_MAX)return false;
|
||||
fragment.y=static_cast<int16_t>(relative_y);
|
||||
_page_layout.push_back(fragment);
|
||||
}
|
||||
}
|
||||
_line_layout.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NomadNetScreen::layout_from(std::size_t start_block,int32_t start_y,
|
||||
int32_t window_top,int32_t window_bottom,
|
||||
bool build_index){
|
||||
constexpr int16_t width=304;
|
||||
int16_t y=0;
|
||||
bool content_remaining=false;
|
||||
for(std::size_t block_index=0;block_index<_page.blocks().size();++block_index){
|
||||
if(_page_layout.size()>=content_fragment_limit){
|
||||
content_remaining=std::any_of(_page.blocks().begin()+block_index,_page.blocks().end(),
|
||||
[](const NomadNet::CompactPage::BlockRecord& candidate){
|
||||
return NomadNet::block_has_layout_content(candidate.type,candidate.run_count);
|
||||
});
|
||||
break;
|
||||
int32_t y=start_y;
|
||||
for(std::size_t block_index=start_block;block_index<_page.blocks().size();++block_index){
|
||||
if(!build_index&&y>=window_bottom)break;
|
||||
if(build_index){
|
||||
if(_layout_checkpoints.size()>=NomadNet::CompactPage::MAX_BLOCKS)return false;
|
||||
_layout_checkpoints.push_back(LayoutCheckpoint{
|
||||
static_cast<uint16_t>(block_index),y});
|
||||
}
|
||||
const auto& block=_page.blocks()[block_index];
|
||||
if(block.type==NomadNet::BlockType::DIVIDER){
|
||||
_page_layout.push_back(LayoutFragment(0,0,0,-1,0,y,width,1,true));y+=9;continue;
|
||||
if(y+1>=window_top&&y<window_bottom){
|
||||
if(_page_layout.size()>=MAX_WINDOW_FRAGMENTS)return false;
|
||||
_page_layout.push_back(LayoutFragment(0,0,0,-1,0,
|
||||
static_cast<int16_t>(y-window_top),width,1,true));
|
||||
}
|
||||
y+=9;continue;
|
||||
}
|
||||
if(block.run_count==0)continue;
|
||||
const int16_t indent=block.type==NomadNet::BlockType::HEADING?0:
|
||||
@@ -384,20 +464,9 @@ bool NomadNetScreen::layout_page(){
|
||||
const int16_t available=width-indent;
|
||||
int16_t x=indent;
|
||||
int16_t line_h=16;
|
||||
std::size_t line_first=_page_layout.size();
|
||||
int16_t line_y=y;
|
||||
auto align_line=[&](std::size_t begin,std::size_t end,int16_t current_y){
|
||||
if(begin>=end||block.alignment==NomadNet::Alignment::LEFT)return;
|
||||
int16_t line_width=0;
|
||||
for(std::size_t i=begin;i<end;++i)if(_page_layout[i].y==current_y)
|
||||
line_width=std::max<int16_t>(line_width,_page_layout[i].x+_page_layout[i].width-indent);
|
||||
const int16_t shift=block.alignment==NomadNet::Alignment::CENTER
|
||||
?(available-line_width)/2:available-line_width;
|
||||
if(shift>0)for(std::size_t i=begin;i<end;++i)
|
||||
if(_page_layout[i].y==current_y)_page_layout[i].x+=shift;
|
||||
};
|
||||
int32_t line_y=y;
|
||||
_line_layout.clear();
|
||||
for(uint16_t r=0;r<block.run_count&&block.first_run+r<_page.runs().size();++r){
|
||||
if(_page_layout.size()>=content_fragment_limit){content_remaining=true;break;}
|
||||
const uint16_t run_index=static_cast<uint16_t>(block.first_run+r);
|
||||
const auto& run=_page.runs()[run_index];
|
||||
const auto text=_page.text(run);
|
||||
@@ -407,7 +476,6 @@ bool NomadNetScreen::layout_page(){
|
||||
line_h=std::max(line_h,height);
|
||||
std::size_t offset=0;
|
||||
while(offset<text.size()){
|
||||
if(_page_layout.size()>=content_fragment_limit){content_remaining=true;break;}
|
||||
std::size_t end=offset;
|
||||
const bool whitespace=text[offset]==' '||text[offset]=='\t';
|
||||
while(end<text.size()&&((text[end]==' '||text[end]=='\t')==whitespace)&&end-offset<255)++end;
|
||||
@@ -416,8 +484,9 @@ bool NomadNetScreen::layout_page(){
|
||||
static_cast<uint32_t>(end-offset),font,0,LV_TEXT_FLAG_NONE));
|
||||
if(whitespace&&x+fragment_w>indent+available){offset=end;continue;}
|
||||
if(!whitespace&&x>indent&&x+fragment_w>indent+available){
|
||||
align_line(line_first,_page_layout.size(),line_y);
|
||||
y+=line_h;x=indent;line_h=height;line_first=_page_layout.size();line_y=y;
|
||||
if(!commit_line(line_y,line_h,block.alignment,indent,available,
|
||||
window_top,window_bottom))return false;
|
||||
y+=line_h;x=indent;line_h=height;line_y=y;
|
||||
}
|
||||
if(fragment_w>available){
|
||||
end=offset;
|
||||
@@ -434,51 +503,82 @@ bool NomadNetScreen::layout_page(){
|
||||
}
|
||||
}
|
||||
if(!(whitespace&&x==indent)){
|
||||
_page_layout.push_back(LayoutFragment(run_index,static_cast<uint16_t>(offset),
|
||||
static_cast<uint16_t>(end-offset),run.link_index,x,y,fragment_w,height,false,
|
||||
large));
|
||||
if(run.link_index>=0&&static_cast<std::size_t>(run.link_index)<_link_y.size()){
|
||||
if(_link_y[run.link_index]<0)_link_y[run.link_index]=line_y;
|
||||
_link_bottom[run.link_index]=std::max(_link_bottom[run.link_index],line_y+height);
|
||||
}
|
||||
if(!append_line_fragment(LayoutFragment(run_index,static_cast<uint16_t>(offset),
|
||||
static_cast<uint16_t>(end-offset),run.link_index,x,0,fragment_w,height,false,
|
||||
large)))return false;
|
||||
x=static_cast<int16_t>(x+fragment_w);
|
||||
}
|
||||
offset=end;
|
||||
}
|
||||
if(content_remaining)break;
|
||||
}
|
||||
align_line(line_first,_page_layout.size(),line_y);
|
||||
if(!commit_line(line_y,line_h,block.alignment,indent,available,
|
||||
window_top,window_bottom))return false;
|
||||
y+=line_h+3;
|
||||
if(content_remaining)break;
|
||||
}
|
||||
if(NomadNet::layout_content_truncated(_page_layout.size(),content_fragment_limit,
|
||||
content_remaining)){
|
||||
const std::string layout_notice="[Page layout truncated: "+
|
||||
std::to_string(content_fragment_limit)+" fragments]";
|
||||
if(!_page.append_notice(layout_notice))return false;
|
||||
const uint16_t notice_run_index=static_cast<uint16_t>(_page.runs().size()-1);
|
||||
_page_layout.erase(std::remove_if(_page_layout.begin(),_page_layout.end(),
|
||||
[notice_run_index](const LayoutFragment& fragment){
|
||||
return !fragment.divider&&fragment.run_index>=notice_run_index;
|
||||
}),_page_layout.end());
|
||||
const auto notice=_page.text(_page.runs()[notice_run_index]);
|
||||
const int16_t notice_width=static_cast<int16_t>(std::min<lv_coord_t>(304,
|
||||
lv_txt_get_width(notice.data(),static_cast<uint32_t>(notice.size()),
|
||||
&nomadnet_font_12,0,LV_TEXT_FLAG_NONE)));
|
||||
_page_layout.push_back(LayoutFragment(notice_run_index,0,
|
||||
static_cast<uint16_t>(notice.size()),-1,0,y,notice_width,19,false));
|
||||
y+=22;
|
||||
}
|
||||
_page_height=std::max<int32_t>(y,lv_obj_get_content_height(_content));
|
||||
if(build_index)_page_height=std::max<int32_t>(y,lv_obj_get_content_height(_content));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NomadNetScreen::layout_window(int32_t logical_scroll){
|
||||
const int32_t viewport=std::max<int32_t>(1,lv_obj_get_content_height(_content));
|
||||
const int32_t window_top=NomadNet::VirtualViewport::window_top(logical_scroll,viewport);
|
||||
const int32_t window_bottom=NomadNet::VirtualViewport::window_bottom(
|
||||
logical_scroll,viewport,_page_height);
|
||||
if(window_top==_layout_window_top&&window_bottom==_layout_window_bottom)return true;
|
||||
std::size_t start_block=0;
|
||||
int32_t start_y=0;
|
||||
for(const auto& checkpoint:_layout_checkpoints){
|
||||
if(checkpoint.y>window_top)break;
|
||||
start_block=checkpoint.block_index;
|
||||
start_y=checkpoint.y;
|
||||
}
|
||||
_page_layout.clear();
|
||||
if(!layout_from(start_block,start_y,window_top,window_bottom,false))return false;
|
||||
_layout_window_top=window_top;
|
||||
_layout_window_bottom=window_bottom;
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t NomadNetScreen::logical_scroll_from_widget()const{
|
||||
return NomadNet::VirtualViewport::logical_from_physical(
|
||||
lv_obj_get_scroll_y(_content),_page_height,
|
||||
lv_obj_get_content_height(_content),_physical_extent);
|
||||
}
|
||||
|
||||
void NomadNetScreen::scroll_to_logical(int32_t logical,lv_anim_enable_t animation){
|
||||
const int32_t viewport=std::max<int32_t>(1,lv_obj_get_content_height(_content));
|
||||
const int32_t logical_max=std::max<int32_t>(0,_page_height-viewport);
|
||||
const int32_t target=std::max<int32_t>(0,std::min(logical,logical_max));
|
||||
const int32_t physical=NomadNet::VirtualViewport::physical_from_logical(
|
||||
target,_page_height,viewport,_physical_extent);
|
||||
if(animation==LV_ANIM_OFF){
|
||||
_logical_scroll=target;
|
||||
layout_window(_logical_scroll);
|
||||
}
|
||||
lv_obj_scroll_to_y(_content,physical,animation);
|
||||
lv_obj_invalidate(_content);
|
||||
}
|
||||
|
||||
void NomadNetScreen::draw_page(lv_event_t* event){
|
||||
auto* draw_ctx=lv_event_get_draw_ctx(event);
|
||||
const int16_t scroll=lv_obj_get_scroll_y(_content);
|
||||
const int16_t top=_content->coords.y1+lv_obj_get_style_pad_top(_content,0);
|
||||
const int16_t left=_content->coords.x1+lv_obj_get_style_pad_left(_content,0);
|
||||
lv_area_t content_area;
|
||||
lv_obj_get_content_coords(_content,&content_area);
|
||||
lv_area_t content_clip;
|
||||
if(!_lv_area_intersect(&content_clip,draw_ctx->clip_area,&content_area))return;
|
||||
const lv_area_t* original_clip=draw_ctx->clip_area;
|
||||
draw_ctx->clip_area=&content_clip;
|
||||
const int16_t top=content_area.y1;
|
||||
const int16_t left=content_area.x1;
|
||||
char scratch[256];
|
||||
for(const auto& fragment:_page_layout){
|
||||
const int16_t draw_y=static_cast<int16_t>(top+fragment.y-scroll);
|
||||
const int32_t logical_y=_layout_window_top+fragment.y;
|
||||
const int32_t draw_y=top+logical_y-_logical_scroll;
|
||||
if(draw_y+fragment.height<_content->coords.y1||draw_y>_content->coords.y2)continue;
|
||||
lv_area_t area{static_cast<lv_coord_t>(left+fragment.x),draw_y,
|
||||
lv_area_t area{static_cast<lv_coord_t>(left+fragment.x),static_cast<lv_coord_t>(draw_y),
|
||||
static_cast<lv_coord_t>(left+fragment.x+std::max<int16_t>(fragment.width,1)-1),
|
||||
static_cast<lv_coord_t>(draw_y+std::max<int16_t>(fragment.height,1)-1)};
|
||||
if(fragment.divider){lv_draw_rect_dsc_t dsc;lv_draw_rect_dsc_init(&dsc);dsc.bg_color=Theme::border();lv_draw_rect(draw_ctx,&dsc,&area);continue;}
|
||||
@@ -500,9 +600,9 @@ void NomadNetScreen::draw_page(lv_event_t* event){
|
||||
if(_selected_link>=0){
|
||||
NomadNet::for_each_focus_span(_page_layout,_selected_link,[&](const NomadNet::FocusSpan& span){
|
||||
if(span.run_index>=_page.runs().size())return;
|
||||
const int16_t draw_y=static_cast<int16_t>(top+span.y-scroll);
|
||||
const int32_t draw_y=top+_layout_window_top+span.y-_logical_scroll;
|
||||
if(draw_y+span.height<_content->coords.y1||draw_y>_content->coords.y2)return;
|
||||
lv_area_t area{static_cast<lv_coord_t>(left+span.x),draw_y,
|
||||
lv_area_t area{static_cast<lv_coord_t>(left+span.x),static_cast<lv_coord_t>(draw_y),
|
||||
static_cast<lv_coord_t>(left+span.x+std::max<int16_t>(span.width,1)-1),
|
||||
static_cast<lv_coord_t>(draw_y+std::max<int16_t>(span.height,1)-1)};
|
||||
lv_draw_rect_dsc_t focus;lv_draw_rect_dsc_init(&focus);focus.bg_opa=LV_OPA_TRANSP;
|
||||
@@ -511,30 +611,28 @@ void NomadNetScreen::draw_page(lv_event_t* event){
|
||||
focus.border_width=1;lv_draw_rect(draw_ctx,&focus,&area);
|
||||
});
|
||||
}
|
||||
draw_ctx->clip_area=original_clip;
|
||||
}
|
||||
|
||||
void NomadNetScreen::select_link(int direction){
|
||||
if(_page.links().empty()){
|
||||
const int next=std::max(0,lv_obj_get_scroll_y(_content)+direction*40);
|
||||
lv_obj_scroll_to_y(_content,next,LV_ANIM_ON);
|
||||
scroll_to_logical(_logical_scroll+direction*40,LV_ANIM_ON);
|
||||
return;
|
||||
}
|
||||
const int count=static_cast<int>(_page.links().size());
|
||||
int candidate=_selected_link;
|
||||
for(int attempt=0;attempt<count;++attempt){
|
||||
candidate=candidate<0?(direction>=0?0:count-1):(candidate+direction+count)%count;
|
||||
bool rendered=false;
|
||||
for(const auto& fragment:_page_layout)if(fragment.link_index==candidate){rendered=true;break;}
|
||||
if(rendered){_selected_link=candidate;break;}
|
||||
const bool laid_out=static_cast<std::size_t>(candidate)<_link_y.size()&&_link_y[candidate]>=0;
|
||||
if(laid_out){_selected_link=candidate;break;}
|
||||
}
|
||||
if(_selected_link<0)return;
|
||||
for(const auto& fragment:_page_layout)if(fragment.link_index==_selected_link){
|
||||
const int top=fragment.y;const int bottom=fragment.y+fragment.height;
|
||||
const int scroll=lv_obj_get_scroll_y(_content);const int visible=lv_obj_get_content_height(_content);
|
||||
if(top<scroll)lv_obj_scroll_to_y(_content,top,LV_ANIM_ON);
|
||||
else if(bottom>scroll+visible)lv_obj_scroll_to_y(_content,bottom-visible,LV_ANIM_ON);
|
||||
break;
|
||||
}
|
||||
const int32_t link_top=_link_y[_selected_link];
|
||||
const int32_t link_bottom=_link_bottom[_selected_link];
|
||||
const int32_t visible=lv_obj_get_content_height(_content);
|
||||
if(link_top<_logical_scroll)scroll_to_logical(link_top,LV_ANIM_ON);
|
||||
else if(link_bottom>_logical_scroll+visible)
|
||||
scroll_to_logical(link_bottom-visible,LV_ANIM_ON);
|
||||
lv_obj_invalidate(_content);
|
||||
}
|
||||
|
||||
@@ -549,7 +647,12 @@ void NomadNetScreen::page_event(lv_event_t* event){
|
||||
auto* self=static_cast<NomadNetScreen*>(lv_event_get_user_data(event));
|
||||
const auto code=lv_event_get_code(event);
|
||||
if(code==LV_EVENT_DRAW_MAIN)self->draw_page(event);
|
||||
else if(code==LV_EVENT_GET_SELF_SIZE){auto* size=static_cast<lv_point_t*>(lv_event_get_param(event));size->y=std::max<lv_coord_t>(size->y,self->_page_height);}
|
||||
else if(code==LV_EVENT_GET_SELF_SIZE){auto* size=static_cast<lv_point_t*>(lv_event_get_param(event));size->y=std::max<lv_coord_t>(size->y,static_cast<lv_coord_t>(self->_physical_extent));}
|
||||
else if(code==LV_EVENT_SCROLL){
|
||||
self->_logical_scroll=self->logical_scroll_from_widget();
|
||||
if(!self->layout_window(self->_logical_scroll))self->set_status("Page viewport could not be retained");
|
||||
lv_obj_invalidate(self->_content);
|
||||
}
|
||||
else if(code==LV_EVENT_FOCUSED){
|
||||
auto* group=static_cast<lv_group_t*>(lv_obj_get_group(self->_content));
|
||||
if(group)lv_group_set_editing(group,true);
|
||||
@@ -571,10 +674,19 @@ void NomadNetScreen::page_event(lv_event_t* event){
|
||||
else if(code==LV_EVENT_CLICKED){
|
||||
auto* indev=lv_indev_get_act();
|
||||
if(!indev||lv_indev_get_type(indev)!=LV_INDEV_TYPE_POINTER)return;
|
||||
lv_point_t point;lv_indev_get_point(indev,&point);const int scroll=lv_obj_get_scroll_y(self->_content);
|
||||
const int x=point.x-self->_content->coords.x1-lv_obj_get_style_pad_left(self->_content,0);
|
||||
const int y=point.y-self->_content->coords.y1-lv_obj_get_style_pad_top(self->_content,0)+scroll;
|
||||
for(const auto& fragment:self->_page_layout)if(fragment.link_index>=0&&x>=fragment.x&&x<fragment.x+fragment.width&&y>=fragment.y&&y<fragment.y+fragment.height){self->_selected_link=fragment.link_index;self->activate_selected_link();break;}
|
||||
lv_point_t point;lv_indev_get_point(indev,&point);
|
||||
lv_area_t content_area;
|
||||
lv_obj_get_content_coords(self->_content,&content_area);
|
||||
if(!_lv_area_is_point_on(&content_area,&point,0))return;
|
||||
const int x=point.x-content_area.x1;
|
||||
const int32_t y=point.y-content_area.y1+self->_logical_scroll;
|
||||
for(const auto& fragment:self->_page_layout){
|
||||
const int32_t fragment_y=self->_layout_window_top+fragment.y;
|
||||
if(fragment.link_index>=0&&x>=fragment.x&&x<fragment.x+fragment.width&&
|
||||
y>=fragment_y&&y<fragment_y+fragment.height){
|
||||
self->_selected_link=fragment.link_index;self->activate_selected_link();break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
void NomadNetScreen::show(){
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "NomadNetCompactPage.h"
|
||||
#include "NomadNetDocument.h"
|
||||
#include "NomadNetLibrary.h"
|
||||
#include "NomadNetVirtualViewport.h"
|
||||
|
||||
namespace UI::LXMF {
|
||||
class NomadNetScreen {
|
||||
@@ -35,9 +36,11 @@ public:
|
||||
bool directory_visible() const { return _directory_visible.load(std::memory_order_acquire); }
|
||||
void show(); void hide();
|
||||
private:
|
||||
// 1024 wrapped fragments remain below LVGL 8.x's signed 16-bit vertical
|
||||
// coordinate range even at the 16 px heading font's 19 px line box.
|
||||
static constexpr std::size_t MAX_LAYOUT_FRAGMENTS = 1024;
|
||||
// Only the visible region plus bounded overscan is retained. The parser
|
||||
// admits at most 1024 runs, so this also covers a pathological viewport
|
||||
// containing every styled run plus bounded dividers.
|
||||
static constexpr std::size_t MAX_WINDOW_FRAGMENTS = 1056;
|
||||
static constexpr int32_t MAX_PHYSICAL_SCROLL_EXTENT = 30000;
|
||||
struct LayoutFragment {
|
||||
uint16_t run_index = 0;
|
||||
uint16_t byte_offset = 0;
|
||||
@@ -56,6 +59,12 @@ private:
|
||||
: run_index(run), byte_offset(offset), byte_length(length), link_index(link),
|
||||
x(left), y(top), width(w), height(h), divider(is_divider), large_font(is_large) {}
|
||||
};
|
||||
struct LayoutCheckpoint {
|
||||
uint16_t block_index = 0;
|
||||
int32_t y = 0;
|
||||
LayoutCheckpoint() = default;
|
||||
LayoutCheckpoint(uint16_t block, int32_t top) : block_index(block), y(top) {}
|
||||
};
|
||||
lv_obj_t* _screen=nullptr; lv_obj_t* _back_button=nullptr; lv_obj_t* _home_button=nullptr;
|
||||
lv_obj_t* _reload_button=nullptr; lv_obj_t* _save_button=nullptr; lv_obj_t* _address_row=nullptr; lv_obj_t* _address=nullptr;
|
||||
lv_obj_t* _go_button=nullptr; lv_obj_t* _address_summary=nullptr; lv_obj_t* _edit_button=nullptr;
|
||||
@@ -63,7 +72,15 @@ private:
|
||||
lv_obj_t* _directory=nullptr;
|
||||
NomadNet::CompactPage _page;
|
||||
NomadNet::ExternalVector<LayoutFragment> _page_layout;
|
||||
NomadNet::ExternalVector<LayoutFragment> _line_layout;
|
||||
NomadNet::ExternalVector<LayoutCheckpoint> _layout_checkpoints;
|
||||
NomadNet::ExternalVector<int32_t> _link_y;
|
||||
NomadNet::ExternalVector<int32_t> _link_bottom;
|
||||
int32_t _page_height = 0;
|
||||
int32_t _physical_extent = 0;
|
||||
int32_t _logical_scroll = 0;
|
||||
int32_t _layout_window_top = 0;
|
||||
int32_t _layout_window_bottom = 0;
|
||||
int16_t _selected_link = -1;
|
||||
std::vector<lv_obj_t*> _directory_focusables;
|
||||
std::vector<std::string> _directory_targets;
|
||||
@@ -82,6 +99,17 @@ private:
|
||||
void clear_document();
|
||||
void clear_directory();
|
||||
bool layout_page();
|
||||
bool layout_window(int32_t logical_scroll);
|
||||
bool layout_from(std::size_t start_block, int32_t start_y,
|
||||
int32_t window_top, int32_t window_bottom,
|
||||
bool build_index);
|
||||
bool append_line_fragment(const LayoutFragment& fragment);
|
||||
bool commit_line(int32_t line_y, int16_t line_height,
|
||||
NomadNet::Alignment alignment, int16_t indent,
|
||||
int16_t available, int32_t window_top,
|
||||
int32_t window_bottom);
|
||||
int32_t logical_scroll_from_widget() const;
|
||||
void scroll_to_logical(int32_t logical, lv_anim_enable_t animation);
|
||||
void draw_page(lv_event_t* event);
|
||||
void select_link(int direction);
|
||||
void activate_selected_link();
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace UI::LXMF::NomadNet {
|
||||
|
||||
class VirtualViewport {
|
||||
public:
|
||||
static int32_t logical_from_physical(int32_t physical_scroll,
|
||||
int32_t logical_height,
|
||||
int32_t viewport_height,
|
||||
int32_t physical_extent) {
|
||||
const int32_t logical_max = std::max<int32_t>(0, logical_height - viewport_height);
|
||||
const int32_t physical_max = std::max<int32_t>(0, physical_extent - viewport_height);
|
||||
if (logical_max == 0 || physical_max == 0) return 0;
|
||||
const int32_t clamped = std::max<int32_t>(0, std::min(physical_scroll, physical_max));
|
||||
if (logical_max == physical_max) return clamped;
|
||||
return static_cast<int32_t>((static_cast<int64_t>(clamped) * logical_max) / physical_max);
|
||||
}
|
||||
|
||||
static int32_t physical_from_logical(int32_t logical_scroll,
|
||||
int32_t logical_height,
|
||||
int32_t viewport_height,
|
||||
int32_t physical_extent) {
|
||||
const int32_t logical_max = std::max<int32_t>(0, logical_height - viewport_height);
|
||||
const int32_t physical_max = std::max<int32_t>(0, physical_extent - viewport_height);
|
||||
if (logical_max == 0 || physical_max == 0) return 0;
|
||||
const int32_t clamped = std::max<int32_t>(0, std::min(logical_scroll, logical_max));
|
||||
if (logical_max == physical_max) return clamped;
|
||||
return static_cast<int32_t>((static_cast<int64_t>(clamped) * physical_max) / logical_max);
|
||||
}
|
||||
|
||||
static int32_t window_top(int32_t logical_scroll, int32_t viewport_height) {
|
||||
return std::max<int32_t>(0, logical_scroll - viewport_height * 2);
|
||||
}
|
||||
|
||||
static int32_t window_bottom(int32_t logical_scroll,
|
||||
int32_t viewport_height,
|
||||
int32_t logical_height) {
|
||||
return std::min<int32_t>(logical_height, logical_scroll + viewport_height * 3);
|
||||
}
|
||||
|
||||
static bool can_coalesce(uint16_t existing_run,
|
||||
uint16_t existing_offset,
|
||||
uint16_t existing_length,
|
||||
uint16_t next_run,
|
||||
uint16_t next_offset) {
|
||||
return existing_run == next_run &&
|
||||
static_cast<uint32_t>(existing_offset) + existing_length == next_offset;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace UI::LXMF::NomadNet
|
||||
@@ -2293,6 +2293,7 @@ void UIManager::nomad_update() {
|
||||
_nomadnet_screen->set_library(_nomad_library);
|
||||
page_applied = _nomadnet_screen->set_page(document);
|
||||
}
|
||||
if (page_applied) _nomad_response.release();
|
||||
nomad_heap_checkpoint("response-page-applied");
|
||||
if (!page_applied) {
|
||||
nomad_stop_transport();
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "NomadNetProtocol.h"
|
||||
#include "NomadNetRequestPolicy.h"
|
||||
#include "NomadNetUrl.h"
|
||||
#include "NomadNetVirtualViewport.h"
|
||||
|
||||
using UI::LXMF::NavigationStack;
|
||||
using UI::LXMF::Route;
|
||||
@@ -42,6 +43,7 @@ using UI::LXMF::NomadNet::for_each_focus_span;
|
||||
using UI::LXMF::NomadNet::ResponseBuffer;
|
||||
using UI::LXMF::NomadNet::RequestPolicy;
|
||||
using UI::LXMF::NomadNet::Url;
|
||||
using UI::LXMF::NomadNet::VirtualViewport;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
int passed = 0;
|
||||
@@ -585,6 +587,35 @@ int main(int argc, char** argv) {
|
||||
check("divider has layout content without a text run",
|
||||
UI::LXMF::NomadNet::block_has_layout_content(BlockType::DIVIDER, 0));
|
||||
|
||||
check("virtual viewport maps short pages without scaling",
|
||||
VirtualViewport::logical_from_physical(640, 1200, 150, 1200) == 640 &&
|
||||
VirtualViewport::physical_from_logical(640, 1200, 150, 1200) == 640);
|
||||
check("virtual viewport maps tall pages through bounded LVGL coordinates",
|
||||
VirtualViewport::logical_from_physical(14925, 60000, 150, 30000) == 29925 &&
|
||||
VirtualViewport::physical_from_logical(29925, 60000, 150, 30000) == 14925);
|
||||
check("virtual viewport clamps logical and physical scroll positions",
|
||||
VirtualViewport::logical_from_physical(40000, 60000, 150, 30000) == 59850 &&
|
||||
VirtualViewport::physical_from_logical(70000, 60000, 150, 30000) == 29850);
|
||||
check("virtual viewport keeps bounded overscan around the visible region",
|
||||
VirtualViewport::window_top(30000, 150) == 29700 &&
|
||||
VirtualViewport::window_bottom(30000, 150, 60000) == 30450);
|
||||
check("overscan window clamps at the document boundaries",
|
||||
VirtualViewport::window_top(5,75)==0 &&
|
||||
VirtualViewport::window_bottom(5,75,60000)==230);
|
||||
|
||||
std::vector<uint8_t> response_bytes(ResponseBuffer::MAX_BYTES,0x5a);
|
||||
check("response buffer accepts the exact body limit",
|
||||
response.assign(response_bytes.data(),response_bytes.size()) &&
|
||||
response.capacity()>=response_bytes.size());
|
||||
response.release();
|
||||
check("response release returns the normalized PSRAM allocation",
|
||||
response.size()==0 && response.capacity()==0);
|
||||
check("coalescing accepts adjacent bytes from the same styled run",
|
||||
VirtualViewport::can_coalesce(7, 12, 5, 7, 17));
|
||||
check("coalescing preserves style and link run boundaries",
|
||||
!VirtualViewport::can_coalesce(7, 12, 5, 8, 17) &&
|
||||
!VirtualViewport::can_coalesce(7, 12, 5, 7, 18));
|
||||
|
||||
check("compact address preserves short values", compact_address("node:/page/a.mu", 32) == "node:/page/a.mu");
|
||||
check("compact address abbreviates destination but preserves path",
|
||||
compact_address("a8d24177d946de4f1f0a0fe1af9a1338:/page/index.mu", 30) ==
|
||||
|
||||
@@ -133,10 +133,12 @@ def test_nomadnet_page_body_uses_one_compact_custom_viewport():
|
||||
assert "lv_label_create(_content)" not in set_page
|
||||
assert "LV_EVENT_DRAW_MAIN" in screen
|
||||
assert "lv_draw_label(" in screen
|
||||
assert "align_line" in screen
|
||||
assert "commit_line" in screen
|
||||
assert "NomadNet::truncation_notice(document)" in set_page
|
||||
assert "if(!_page.append_notice(notice))" in set_page
|
||||
assert "if(!layout_page())" in set_page
|
||||
assert "catch(const std::bad_alloc&)" in set_page
|
||||
assert "Page is too large for available memory" in set_page
|
||||
assert "[Page truncated to device safety limits]" not in set_page
|
||||
assert "[Unsupported Micron content]" in screen
|
||||
assert "lv_obj_set_scroll_dir(_content,LV_DIR_VER)" in screen
|
||||
@@ -150,17 +152,36 @@ def test_nomadnet_page_body_uses_one_compact_custom_viewport():
|
||||
assert "const std::string rendered_text" not in compact_cpp
|
||||
assert "visit_display_text" in compact_cpp
|
||||
assert "lv_indev_get_type(indev)!=LV_INDEV_TYPE_POINTER" in screen
|
||||
assert "const std::string layout_notice=\"[Page layout truncated: \"+" in screen
|
||||
assert "std::to_string(content_fragment_limit)+\" fragments]\"" in screen
|
||||
assert "fragment.run_index>=notice_run_index" in screen
|
||||
assert "notice_run_index" in screen
|
||||
assert "_page_layout.push_back(LayoutFragment(notice_run_index" in screen
|
||||
assert "MAX_WINDOW_FRAGMENTS" in screen_h
|
||||
assert "MAX_LAYOUT_FRAGMENTS" not in screen_h
|
||||
assert "ExternalVector<LayoutCheckpoint> _layout_checkpoints" in screen_h
|
||||
assert "_layout_window_top" in screen_h
|
||||
assert "_logical_scroll" in screen_h
|
||||
assert "ExternalVector<int32_t> _link_bottom" in screen_h
|
||||
assert "_link_bottom[run.link_index]=std::max" in screen
|
||||
assert "link_top+19" not in screen
|
||||
assert "layout_window(" in screen
|
||||
assert "VirtualViewport::logical_from_physical" in screen
|
||||
assert "VirtualViewport::physical_from_logical" in screen
|
||||
assert "lv_obj_get_content_coords(_content,&content_area)" in screen
|
||||
assert "draw_ctx->clip_area=&content_clip" in screen
|
||||
assert "_lv_area_is_point_on(&content_area,&point,0)" in screen
|
||||
assert "[Page layout truncated:" not in screen
|
||||
assert "content_fragment_limit" not in screen
|
||||
assert "whitespace&&x+fragment_w>indent+available" in screen
|
||||
assert "for(int attempt=0;attempt<count;++attempt)" in screen
|
||||
assert "fragment.link_index==candidate" in screen
|
||||
assert "_link_y[candidate]>=0" in screen
|
||||
assert "lv_group_set_editing(group,false)" in screen
|
||||
|
||||
|
||||
def test_successful_page_application_releases_normalized_response():
|
||||
manager = (INCLUDE / "UIManager.cpp").read_text()
|
||||
apply_start = manager.index("bool page_applied = false;")
|
||||
apply_end = manager.index("nomad_heap_checkpoint(\"response-page-applied\")", apply_start)
|
||||
applied = manager[apply_start:apply_end]
|
||||
assert "_nomad_response.release();" in applied
|
||||
|
||||
|
||||
def test_directory_rebuild_has_one_final_focus_owner():
|
||||
"""Returning from Heard Nodes must not focus every surviving/start row."""
|
||||
screen = (INCLUDE / "NomadNetScreen.cpp").read_text()
|
||||
|
||||
Reference in new issue
Block a user