Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions include/graphics/view/TFT/TFTView_320x240.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
151 changes: 137 additions & 14 deletions source/graphics/TFT/TFTView_320x240.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<TFTView_320x240 *>(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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
}
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down