diff --git a/lib/tdeck_ui/UI/LXMF/NomadNetFocus.h b/lib/tdeck_ui/UI/LXMF/NomadNetFocus.h new file mode 100644 index 00000000..c54573e0 --- /dev/null +++ b/lib/tdeck_ui/UI/LXMF/NomadNetFocus.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +namespace UI::LXMF::NomadNet { + +struct FocusSpan { + int16_t x = 0; + int16_t y = 0; + int16_t width = 0; + int16_t height = 0; + uint16_t run_index = 0; + + FocusSpan() = default; + constexpr FocusSpan(int16_t x_value, int16_t y_value, int16_t width_value, + int16_t height_value, uint16_t run_index_value) + : x(x_value), y(y_value), width(width_value), height(height_value), + run_index(run_index_value) {} +}; + +template +void for_each_focus_span(const FragmentRange& fragments, int16_t selected_link, + Callback callback) { + std::size_t index = 0; + while (index < fragments.size()) { + const auto& first = fragments[index]; + if (first.link_index != selected_link) { + ++index; + continue; + } + + int32_t left = first.x; + int32_t top = first.y; + int32_t right = left + std::max(first.width, 1); + int32_t bottom = top + std::max(first.height, 1); + std::size_t end = index + 1; + while (end < fragments.size()) { + const auto& fragment = fragments[end]; + if (fragment.link_index != selected_link || fragment.y != first.y || + fragment.run_index != first.run_index) break; + left = std::min(left, fragment.x); + right = std::max(right, + static_cast(fragment.x) + std::max(fragment.width, 1)); + bottom = std::max(bottom, + static_cast(fragment.y) + std::max(fragment.height, 1)); + ++end; + } + + callback(FocusSpan{ + static_cast(left), + static_cast(top), + static_cast(right - left), + static_cast(bottom - top), + first.run_index, + }); + index = end; + } +} + +} // namespace UI::LXMF::NomadNet diff --git a/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp b/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp index b8fe28f3..638c2b2f 100644 --- a/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp @@ -3,6 +3,7 @@ #include "Theme.h" #include "NomadNetColors.h" #include "NomadNetDisplay.h" +#include "NomadNetFocus.h" #include "NomadNetGlyphs.h" #include "../LVGL/LVGLInit.h" #include "../TextAreaHelper.h" @@ -454,15 +455,9 @@ void NomadNetScreen::draw_page(lv_event_t* event){ const auto text=_page.text(run); if(fragment.byte_offset+fragment.byte_length>text.size()||fragment.byte_length>=sizeof(scratch))continue; std::memcpy(scratch,text.data()+fragment.byte_offset,fragment.byte_length);scratch[fragment.byte_length]='\0'; - const bool selected=fragment.link_index>=0&&fragment.link_index==_selected_link; if(run.style&NomadNet::CompactPage::HAS_BACKGROUND){ lv_draw_rect_dsc_t bg;lv_draw_rect_dsc_init(&bg);bg.bg_color=lv_color_hex(run.background);lv_draw_rect(draw_ctx,&bg,&area); } - if(selected){ - lv_draw_rect_dsc_t focus;lv_draw_rect_dsc_init(&focus);focus.bg_opa=LV_OPA_TRANSP; - focus.border_color=lv_color_hex(NomadNet::resolve_focus_border( - _page,run,Theme::SURFACE));focus.border_width=1;lv_draw_rect(draw_ctx,&focus,&area); - } lv_draw_label_dsc_t dsc;lv_draw_label_dsc_init(&dsc); dsc.font=page_run_font(run, fragment.large_font); dsc.color=lv_color_hex(NomadNet::resolve_foreground(_page,run,Theme::TEXT_PRIMARY)); @@ -470,6 +465,20 @@ void NomadNetScreen::draw_page(lv_event_t* event){ dsc.decor=(run.style&NomadNet::CompactPage::UNDERLINE)||run.link_index>=0?LV_TEXT_DECOR_UNDERLINE:LV_TEXT_DECOR_NONE; lv_draw_label(draw_ctx,&dsc,&area,scratch,nullptr); } + 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(top+span.y-scroll); + if(draw_y+span.height<_content->coords.y1||draw_y>_content->coords.y2)return; + lv_area_t area{static_cast(left+span.x),draw_y, + static_cast(left+span.x+std::max(span.width,1)-1), + static_cast(draw_y+std::max(span.height,1)-1)}; + lv_draw_rect_dsc_t focus;lv_draw_rect_dsc_init(&focus);focus.bg_opa=LV_OPA_TRANSP; + focus.border_color=lv_color_hex(NomadNet::resolve_focus_border( + _page,_page.runs()[span.run_index],Theme::SURFACE)); + focus.border_width=1;lv_draw_rect(draw_ctx,&focus,&area); + }); + } } void NomadNetScreen::select_link(int direction){ diff --git a/tests/native/test_app_launcher_nomadnet.cpp b/tests/native/test_app_launcher_nomadnet.cpp index 60370dc3..80da4729 100644 --- a/tests/native/test_app_launcher_nomadnet.cpp +++ b/tests/native/test_app_launcher_nomadnet.cpp @@ -16,6 +16,7 @@ #include "NomadNetMailbox.h" #include "NomadNetCompactPage.h" #include "NomadNetColors.h" +#include "NomadNetFocus.h" #include "NomadNetProtocol.h" #include "NomadNetRequestPolicy.h" #include "NomadNetUrl.h" @@ -37,6 +38,7 @@ using UI::LXMF::NomadNet::page_title; using UI::LXMF::NomadNet::AsyncMailbox; using UI::LXMF::NomadNet::CompactPage; using UI::LXMF::NomadNet::resolve_foreground; +using UI::LXMF::NomadNet::for_each_focus_span; using UI::LXMF::NomadNet::ResponseBuffer; using UI::LXMF::NomadNet::RequestPolicy; using UI::LXMF::NomadNet::Url; @@ -239,6 +241,38 @@ int main(int argc, char** argv) { check("focus border uses sRGB contrast for saturated blue", resolve_focus_border(color_page, saturated_blue_run, 0x1d1a1e) == 0xffffff); + struct FocusFragmentFixture { + int16_t link_index; + uint16_t run_index; + int16_t x; + int16_t y; + int16_t width; + int16_t height; + }; + const std::vector focus_fragments{ + {3, 7, 10, 20, 28, 16}, + {3, 7, 38, 20, 7, 16}, + {3, 7, 45, 20, 35, 16}, + {3, 7, 10, 39, 24, 16}, + {3, 8, 34, 39, 20, 16}, + {4, 9, 60, 39, 30, 16}, + }; + std::vector focus_spans; + for_each_focus_span(focus_fragments, 3, [&](const auto& span) { + focus_spans.push_back(span); + }); + check("selected multi-word links use one focus outline per visual line and style run", + focus_spans.size() == 3 && + focus_spans[0].x == 10 && focus_spans[0].y == 20 && + focus_spans[0].width == 70 && focus_spans[0].height == 16 && + focus_spans[0].run_index == 7 && + focus_spans[1].x == 10 && focus_spans[1].y == 39 && + focus_spans[1].width == 24 && focus_spans[1].height == 16 && + focus_spans[1].run_index == 7 && + focus_spans[2].x == 34 && focus_spans[2].y == 39 && + focus_spans[2].width == 20 && focus_spans[2].height == 16 && + focus_spans[2].run_index == 8); + auto reset_color_doc = parser.parse( "#!fg=abc\n`Ff00inline`fpage `[unstyled link`:/page/plain.mu]\n"); CompactPage reset_color_page;