diff --git a/include/graphics/view/TFT/TFTView_320x240.h b/include/graphics/view/TFT/TFTView_320x240.h index ba6cb4bf..4646093c 100644 --- a/include/graphics/view/TFT/TFTView_320x240.h +++ b/include/graphics/view/TFT/TFTView_320x240.h @@ -379,6 +379,8 @@ class TFTView_320x240 : public MeshtasticView static void ui_event_mapNodeButton(lv_event_t *e); static void ui_event_chatNodeButton(lv_event_t *e); static void ui_event_positionButton(lv_event_t *e); + static void ui_event_nodesPanelScroll(lv_event_t *e); + static void ui_event_messageContainerScroll(lv_event_t *e); // animations static void ui_anim_node_panel_cb(void *var, int32_t v); @@ -399,6 +401,8 @@ class TFTView_320x240 : public MeshtasticView uint32_t nodesFiltered; // no. hidden nodes in node list bool nodesChanged; // true if nodes changed (added or purged) bool processingFilter; // indicates that filtering is ongoing + uint16_t nodesScrollDisplayLimit; // current node display limit for infinite scroll + bool nodesScrollLoadingMore; // prevent loading multiple batches simultaneously bool packetLogEnabled; // display received packets bool detectorRunning; // meshDetector is active bool cardDetected; // SD has been detected diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index e65e60bf..eb897c78 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -137,8 +137,8 @@ TFTView_320x240 *TFTView_320x240::instance(const DisplayDriverConfig &cfg) TFTView_320x240::TFTView_320x240(const DisplayDriverConfig *cfg, DisplayDriver *driver) : MeshtasticView(cfg, driver, new ViewController), screensInitialised(false), nodesFiltered(0), nodesChanged(true), - processingFilter(false), packetLogEnabled(false), detectorRunning(false), cardDetected(false), formatSD(false), - packetCounter(0), actTime(0), uptime(0), lastHeard(0), hasPosition(false), myLatitude(0), myLongitude(0), + processingFilter(false), nodesScrollDisplayLimit(50), packetLogEnabled(false), detectorRunning(false), cardDetected(false), + formatSD(false), packetCounter(0), actTime(0), uptime(0), lastHeard(0), hasPosition(false), myLatitude(0), myLongitude(0), topNodeLL(nullptr), scans(0), selectedHops(0), chooseNodeSignalScanner(false), chooseNodeTraceRoute(false), qr(nullptr), db{} { @@ -169,7 +169,8 @@ void TFTView_320x240::init(IClientBase *client) ui_init_boot(); FileLoader::init(&fileSystem); - FileLoader::loadBootImage(objects.boot_logo); + if (!FileLoader::loadBootImage(objects.boot_logo)) + lv_image_set_src(objects.boot_logo, &img_meshtastic_boot_logo_image); // if boot logo is too big remove the label and center the image lv_obj_update_layout(objects.boot_logo); if (lv_obj_get_height(objects.boot_logo) > lv_display_get_vertical_resolution(displaydriver->getDisplay()) / 2) { @@ -728,6 +729,9 @@ void TFTView_320x240::ui_events_init(void) // node and channel buttons lv_obj_add_event_cb(objects.node_button, ui_event_NodeButton, LV_EVENT_ALL, (void *)ownNode); + // nodes panel - infinite scroll support (progressive loading when scrolling near bottom) + lv_obj_add_event_cb(objects.nodes_panel, TFTView_320x240::ui_event_nodesPanelScroll, LV_EVENT_SCROLL, this); + // 8 channel buttons lv_obj_add_event_cb(objects.channel_button0, ui_event_ChannelButton, LV_EVENT_ALL, (void *)0); lv_obj_add_event_cb(objects.channel_button1, ui_event_ChannelButton, LV_EVENT_ALL, (void *)1); @@ -918,7 +922,7 @@ void TFTView_320x240::timer_event_shutdown(lv_timer_t *timer) THIS->controller->stop(); delay(1000); #if defined(ARCH_PORTDUINO) - exit(0); + exit(2); #elif defined(ARCH_ESP32) esp_deep_sleep_start(); #else @@ -1026,6 +1030,26 @@ void TFTView_320x240::ui_event_NodeButton(lv_event_t *e) if (!nodeNum) // event-handler for own node has value 0 in user_data nodeNum = THIS->ownNode; lv_obj_t *panel = THIS->nodes[nodeNum]; + + // Check if click was in the image area (left ~40px) to open chat + lv_indev_t *indev = lv_indev_active(); + if (indev && nodeNum != THIS->ownNode) { + lv_point_t point; + lv_indev_get_point(indev, &point); + lv_area_t coords; + lv_obj_get_coords(panel, &coords); // coords are already in screen space + lv_coord_t local_x = point.x - coords.x1; + + // If click is in the left 40px (image area), open chat instead of expand + if (local_x < 40) { + bool isMessagable = !((unsigned long)(panel->LV_OBJ_IDX(node_img_idx)->user_data) == eRole::unmessagable); + if (isMessagable) { + THIS->showMessages(nodeNum); + return; + } + } + } + if (currentPanel) { // create animation to shrink other panel animRunning = true; @@ -1070,13 +1094,8 @@ void TFTView_320x240::ui_event_NodeButton(lv_event_t *e) THIS->chooseNodeTraceRoute = false; ui_event_trace_route(NULL); } - } else if (event_code == LV_EVENT_LONG_PRESSED) { - // set color and text of clicked node - uint32_t nodeNum = (unsigned long)e->user_data; - bool isMessagable = !((unsigned long)(THIS->nodes[nodeNum]->LV_OBJ_IDX(node_img_idx)->user_data) == eRole::unmessagable); - if (nodeNum != THIS->ownNode && isMessagable) - THIS->showMessages(nodeNum); } + // Note: Long press to chat removed - now use image click instead } void TFTView_320x240::ui_event_GroupsButton(lv_event_t *e) @@ -1212,6 +1231,25 @@ void TFTView_320x240::ui_event_ChatButton(lv_event_t *e) ignoreClicked = false; return; } + + // Ignore click if this interaction was actually a scroll gesture + lv_indev_t *indev = lv_indev_active(); + if (indev && lv_indev_get_scroll_obj(indev)) { + return; // treat as scroll, not click + } + + // Only treat as a click when the touch is inside the label bounds (avoid blank gutter clicks) + lv_obj_t *label = target->LV_OBJ_IDX(0); // chats_button_label + if (indev && label) { + lv_point_t pt; + lv_indev_get_point(indev, &pt); + lv_area_t la; + lv_obj_get_coords(label, &la); // screen coords of label + if (!(pt.x >= la.x1 && pt.x <= la.x2 && pt.y >= la.y1 && pt.y <= la.y2)) { + return; // touched outside label area, ignore + } + } + lv_obj_set_style_border_color(target, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); uint32_t channelOrNode = (unsigned long)e->user_data; @@ -2378,6 +2416,65 @@ void TFTView_320x240::ui_event_positionButton(lv_event_t *e) } } +/** + * @brief Hard object culling for nodes panel + * Actually DELETE nodes outside viewport buffer, recreate when scrolling back + * Dramatically reduces LVGL object count and layout cost + * Trade-off: slight delay when recreating nodes (but faster overall) + */ +void TFTView_320x240::ui_event_nodesPanelScroll(lv_event_t *e) +{ + auto *self = static_cast(lv_event_get_user_data(e)); + if (!self) + return; + + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code != LV_EVENT_SCROLL) + return; + + lv_obj_t *panel = lv_event_get_target_obj(e); + + // Re-entry guard + static bool scroll_running = false; + if (scroll_running) + return; + scroll_running = true; + + const lv_coord_t LOAD_DISTANCE = 300; + + // Load more nodes when approaching bottom + if (lv_obj_get_scroll_bottom(panel) < LOAD_DISTANCE && self->nodesScrollDisplayLimit < MAX_NUM_NODES_VIEW && + !self->nodesScrollLoadingMore) { + + self->nodesScrollLoadingMore = true; + + uint16_t new_limit = self->nodesScrollDisplayLimit + 15; + if (new_limit > MAX_NUM_NODES_VIEW) { + new_limit = MAX_NUM_NODES_VIEW; + } + + self->nodesScrollDisplayLimit = new_limit; + self->updateNodesFiltered(false); + + ILOG_DEBUG("Loaded nodes batch, now displaying %d nodes", self->nodesScrollDisplayLimit); + + self->nodesScrollLoadingMore = false; + } + + scroll_running = false; +} + +/** + * @brief Scroll event handler for message containers + * Progressive loading could be added here if needed for very long message lists + */ +void TFTView_320x240::ui_event_messageContainerScroll(lv_event_t *e) +{ + // Currently no special handling needed - LVGL handles scrolling efficiently + // This handler is kept as a placeholder for future optimizations if needed + (void)e; +} + void TFTView_320x240::ui_screen_event_cb(lv_event_t *e) { if (THIS->activePanel == objects.map_panel) { @@ -4685,6 +4782,15 @@ void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShor lv_obj_set_style_align(ui_Telemetry2Label, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(ui_Telemetry2Label, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + // optimisation: hide all 6ix extended labels by default; enable only when set + // lv_obj_add_flag(ui_lastHeardLabel, LV_OBJ_FLAG_HIDDEN); // lastHeard + lv_obj_add_flag(ui_BatteryLabel, LV_OBJ_FLAG_HIDDEN); // Autohide battery + lv_obj_add_flag(ui_SignalLabel, LV_OBJ_FLAG_HIDDEN); // Autohide signal/hops + lv_obj_add_flag(ui_PositionLabel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Position2Label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Telemetry1Label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Telemetry2Label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_event_cb(nodeButton, ui_event_NodeButton, LV_EVENT_ALL, (void *)nodeNum); // move node into new position within nodePanel @@ -4882,6 +4988,8 @@ void TFTView_320x240::updatePosition(uint32_t nodeNum, int32_t lat, int32_t lon, // store lat/lon in user_data, because we need these values later to calculate the distance to us panel->LV_OBJ_IDX(node_pos1_idx)->user_data = (void *)lat; panel->LV_OBJ_IDX(node_pos2_idx)->user_data = (void *)lon; + lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos1_idx), LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos2_idx), LV_OBJ_FLAG_HIDDEN); } applyNodesFilter(nodeNum); @@ -4991,6 +5099,7 @@ void TFTView_320x240::updateMetrics(uint32_t nodeNum, uint32_t bat_level, float bat_level = std::min(bat_level, (uint32_t)100); sprintf(buf, "%d%% %0.2fV", bat_level, voltage); lv_label_set_text(it->second->LV_OBJ_IDX(node_bat_idx), buf); + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_bat_idx), LV_OBJ_FLAG_HIDDEN); } } } @@ -5016,11 +5125,13 @@ void TFTView_320x240::updateEnvironmentMetrics(uint32_t nodeNum, const meshtasti } } lv_label_set_text(it->second->LV_OBJ_IDX(node_tm1_idx), buf); + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm1_idx), LV_OBJ_FLAG_HIDDEN); if (metrics.iaq > 0 && metrics.iaq < 1000) { sprintf(buf, "IAQ: %d %.1fV %.1fmA", metrics.iaq, metrics.voltage, metrics.current); lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); it->second->LV_OBJ_IDX(node_tm2_idx)->user_data = (void *)(uint32_t)metrics.iaq; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm2_idx), LV_OBJ_FLAG_HIDDEN); } applyNodesFilter(nodeNum); } @@ -5064,6 +5175,7 @@ void TFTView_320x240::updateSignalStrength(uint32_t nodeNum, int32_t rssi, float } lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); it->second->LV_OBJ_IDX(node_sig_idx)->user_data = 0; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); } } } @@ -5077,6 +5189,7 @@ void TFTView_320x240::updateHopsAway(uint32_t nodeNum, uint8_t hopsAway) sprintf(buf, _("hops: %d"), (int)hopsAway); lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); it->second->LV_OBJ_IDX(node_sig_idx)->user_data = (void *)(unsigned long)hopsAway; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); } } } @@ -6434,12 +6547,14 @@ void TFTView_320x240::newMessage(uint32_t nodeNum, lv_obj_t *container, uint8_t lv_obj_set_align(msgLabel, LV_ALIGN_LEFT_MID); lv_label_set_text(msgLabel, msg); add_style_new_message_style(msgLabel); + lv_obj_add_flag(msgLabel, LV_OBJ_FLAG_CLICKABLE); // enable tap on bubble + // Only the label (bubble) is clickable to open sender node; taps in gutter are ignored + lv_obj_add_event_cb(msgLabel, ui_event_chatNodeButton, LV_EVENT_CLICKED, (void *)nodeNum); if (state == MeshtasticView::eRunning) { lv_obj_scroll_to_view(hiddenPanel, LV_ANIM_ON); lv_obj_move_foreground(objects.message_input_area); } - lv_obj_add_event_cb(hiddenPanel, ui_event_chatNodeButton, LV_EVENT_CLICKED, (void *)nodeNum); } /** @@ -6546,6 +6661,7 @@ void TFTView_320x240::addChat(uint32_t from, uint32_t to, uint8_t ch) lv_obj_set_style_pad_bottom(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_row(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_column(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(chatBtn, lv_color_hex(0xff141723), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_move_to_index(chatBtn, 0); char buf[64]; @@ -6899,7 +7015,6 @@ void TFTView_320x240::setGroupFocus(lv_obj_t *panel) lv_group_focus_obj(panel->spec_attr->children[1]); // TODO: does not work } } else if (panel == objects.map_panel) { - } else if (panel == objects.settings_screen_lock_panel) { lv_group_focus_obj(objects.screen_lock_button_matrix); } else if (panel == objects.controller_panel) { @@ -7020,19 +7135,27 @@ void TFTView_320x240::updateNodesStatus(void) void TFTView_320x240::updateNodesFiltered(bool reset) { static auto it = nodes.begin(); + static uint16_t processedCount = 0; // Track how many nodes we've processed in current session + if (reset || nodesChanged) { nodesFiltered = 0; nodesChanged = false; processingFilter = true; it = nodes.begin(); + processedCount = 0; } - for (int i = 0; i < 10 && it != nodes.end(); i++) { + // Process nodes in batches of 10, but respect the display limit for infinite scroll + uint16_t batchSize = 10; + uint16_t processLimit = nodesScrollDisplayLimit; // Limit based on infinite scroll display count + + for (int i = 0; i < batchSize && it != nodes.end() && processedCount < processLimit; i++) { applyNodesFilter(it->first, true); it++; + processedCount++; } - if (it == nodes.end()) { + if (it == nodes.end() || processedCount >= processLimit) { processingFilter = false; } updateNodesStatus();