mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-14 06:42:34 +00:00
Manual message scrolling
This commit is contained in:
@@ -1503,7 +1503,20 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
menuHandler::handleMenuSwitch(dispdev);
|
menuHandler::handleMenuSwitch(dispdev);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||||
|
|
||||||
|
if (event->inputEvent == INPUT_BROKER_UP) {
|
||||||
|
graphics::MessageRenderer::scrollUp();
|
||||||
|
setFastFramerate(); // match existing behavior
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->inputEvent == INPUT_BROKER_DOWN) {
|
||||||
|
graphics::MessageRenderer::scrollDown();
|
||||||
|
setFastFramerate();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Use left or right input from a keyboard to move between frames,
|
// Use left or right input from a keyboard to move between frames,
|
||||||
// so long as a mesh module isn't using these events for some other purpose
|
// so long as a mesh module isn't using these events for some other purpose
|
||||||
if (showingNormalScreen) {
|
if (showingNormalScreen) {
|
||||||
|
|||||||
@@ -220,6 +220,10 @@ class Screen : public concurrency::OSThread
|
|||||||
public:
|
public:
|
||||||
OLEDDisplay *getDisplayDevice() { return dispdev; }
|
OLEDDisplay *getDisplayDevice() { return dispdev; }
|
||||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||||
|
|
||||||
|
// Screen dimension accessors
|
||||||
|
inline int getHeight() const { return displayHeight; }
|
||||||
|
inline int getWidth() const { return displayWidth; }
|
||||||
size_t frameCount = 0; // Total number of active frames
|
size_t frameCount = 0; // Total number of active frames
|
||||||
~Screen();
|
~Screen();
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ namespace MessageRenderer
|
|||||||
|
|
||||||
static std::vector<std::string> cachedLines;
|
static std::vector<std::string> cachedLines;
|
||||||
static std::vector<int> cachedHeights;
|
static std::vector<int> cachedHeights;
|
||||||
|
static bool manualScrolling = false;
|
||||||
|
|
||||||
// UTF-8 skip helper
|
// UTF-8 skip helper
|
||||||
static inline size_t utf8CharLen(uint8_t c)
|
static inline size_t utf8CharLen(uint8_t c)
|
||||||
@@ -73,6 +74,41 @@ std::string normalizeEmoji(const std::string &s)
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scroll state (file scope so we can reset on new message)
|
||||||
|
float scrollY = 0.0f;
|
||||||
|
uint32_t lastTime = 0;
|
||||||
|
uint32_t scrollStartDelay = 0;
|
||||||
|
uint32_t pauseStart = 0;
|
||||||
|
bool waitingToReset = false;
|
||||||
|
bool scrollStarted = false;
|
||||||
|
static bool didReset = false;
|
||||||
|
|
||||||
|
void scrollUp()
|
||||||
|
{
|
||||||
|
manualScrolling = true;
|
||||||
|
scrollY -= 12;
|
||||||
|
if (scrollY < 0)
|
||||||
|
scrollY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scrollDown()
|
||||||
|
{
|
||||||
|
manualScrolling = true;
|
||||||
|
|
||||||
|
int totalHeight = 0;
|
||||||
|
for (int h : cachedHeights)
|
||||||
|
totalHeight += h;
|
||||||
|
|
||||||
|
int visibleHeight = screen->getHeight() - (FONT_HEIGHT_SMALL * 2);
|
||||||
|
int maxScroll = totalHeight - visibleHeight;
|
||||||
|
if (maxScroll < 0)
|
||||||
|
maxScroll = 0;
|
||||||
|
|
||||||
|
scrollY += 12;
|
||||||
|
if (scrollY > maxScroll)
|
||||||
|
scrollY = maxScroll;
|
||||||
|
}
|
||||||
|
|
||||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
||||||
{
|
{
|
||||||
std::string renderLine;
|
std::string renderLine;
|
||||||
@@ -117,7 +153,6 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
|
|||||||
int lineHeight = std::max(fontHeight, maxIconHeight);
|
int lineHeight = std::max(fontHeight, maxIconHeight);
|
||||||
int baselineOffset = (lineHeight - fontHeight) / 2;
|
int baselineOffset = (lineHeight - fontHeight) / 2;
|
||||||
int fontY = y + baselineOffset;
|
int fontY = y + baselineOffset;
|
||||||
int fontMidline = fontY + fontHeight / 2;
|
|
||||||
|
|
||||||
// Step 3: Render line in segments
|
// Step 3: Render line in segments
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
@@ -191,15 +226,6 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll state (file scope so we can reset on new message)
|
|
||||||
float scrollY = 0.0f;
|
|
||||||
uint32_t lastTime = 0;
|
|
||||||
uint32_t scrollStartDelay = 0;
|
|
||||||
uint32_t pauseStart = 0;
|
|
||||||
bool waitingToReset = false;
|
|
||||||
bool scrollStarted = false;
|
|
||||||
static bool didReset = false; // <-- add here
|
|
||||||
|
|
||||||
// Reset scroll state when new messages arrive
|
// Reset scroll state when new messages arrive
|
||||||
void resetScrollState()
|
void resetScrollState()
|
||||||
{
|
{
|
||||||
@@ -208,7 +234,7 @@ void resetScrollState()
|
|||||||
waitingToReset = false;
|
waitingToReset = false;
|
||||||
scrollStartDelay = millis();
|
scrollStartDelay = millis();
|
||||||
lastTime = millis();
|
lastTime = millis();
|
||||||
|
manualScrolling = false;
|
||||||
didReset = false;
|
didReset = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +381,22 @@ static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &
|
|||||||
return totalWidth;
|
return totalWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY)
|
||||||
|
{
|
||||||
|
if (totalHeight <= visibleHeight)
|
||||||
|
return; // no scrollbar needed
|
||||||
|
|
||||||
|
int scrollbarX = display->getWidth() - 2;
|
||||||
|
int scrollbarHeight = visibleHeight;
|
||||||
|
int thumbHeight = std::max(6, (scrollbarHeight * visibleHeight) / totalHeight);
|
||||||
|
int maxScroll = std::max(1, totalHeight - visibleHeight);
|
||||||
|
int thumbY = startY + (scrollbarHeight - thumbHeight) * scrollOffset / maxScroll;
|
||||||
|
|
||||||
|
for (int i = 0; i < thumbHeight; i++) {
|
||||||
|
display->setPixel(scrollbarX, thumbY + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
// Ensure any boot-relative timestamps are upgraded if RTC is valid
|
// Ensure any boot-relative timestamps are upgraded if RTC is valid
|
||||||
@@ -594,7 +636,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
if (!scrollStarted && now - scrollStartDelay > 2000)
|
if (!scrollStarted && now - scrollStartDelay > 2000)
|
||||||
scrollStarted = true;
|
scrollStarted = true;
|
||||||
|
|
||||||
if (totalHeight > usableScrollHeight) {
|
if (!manualScrolling && totalHeight > usableScrollHeight) {
|
||||||
if (scrollStarted) {
|
if (scrollStarted) {
|
||||||
if (!waitingToReset) {
|
if (!waitingToReset) {
|
||||||
scrollY += delta * scrollSpeed;
|
scrollY += delta * scrollSpeed;
|
||||||
@@ -610,7 +652,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
scrollStartDelay = lastTime;
|
scrollStartDelay = lastTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!manualScrolling) {
|
||||||
|
// Only reset when autoscroll is disabled AND user isn't manually scrolling
|
||||||
scrollY = 0;
|
scrollY = 0;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
@@ -618,11 +661,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
scrollY = 0.0f;
|
scrollY = 0.0f;
|
||||||
waitingToReset = false;
|
waitingToReset = false;
|
||||||
scrollStarted = false;
|
scrollStarted = false;
|
||||||
lastTime = millis(); // keep timebase sane
|
lastTime = millis();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int scrollOffset = static_cast<int>(scrollY);
|
int finalScroll = (int)scrollY;
|
||||||
int yOffset = -scrollOffset + getTextPositions(display)[1];
|
int yOffset = -finalScroll + getTextPositions(display)[1];
|
||||||
|
|
||||||
// Render visible lines
|
// Render visible lines
|
||||||
for (size_t i = 0; i < cachedLines.size(); ++i) {
|
for (size_t i = 0; i < cachedLines.size(); ++i) {
|
||||||
@@ -632,9 +675,19 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
|
|
||||||
if (lineY > -cachedHeights[i] && lineY < scrollBottom) {
|
if (lineY > -cachedHeights[i] && lineY < scrollBottom) {
|
||||||
if (isHeader[i]) {
|
if (isHeader[i]) {
|
||||||
// Render header
|
|
||||||
int w = display->getStringWidth(cachedLines[i].c_str());
|
int w = display->getStringWidth(cachedLines[i].c_str());
|
||||||
int headerX = isMine[i] ? (SCREEN_WIDTH - w - 2) : x;
|
|
||||||
|
// Render header
|
||||||
|
constexpr int SCROLLBAR_WIDTH = 3;
|
||||||
|
|
||||||
|
int headerX;
|
||||||
|
if (isMine[i]) {
|
||||||
|
// push header left to avoid overlap with scrollbar
|
||||||
|
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH) - w - 2;
|
||||||
|
} else {
|
||||||
|
headerX = x;
|
||||||
|
}
|
||||||
display->drawString(headerX, lineY, cachedLines[i].c_str());
|
display->drawString(headerX, lineY, cachedLines[i].c_str());
|
||||||
|
|
||||||
// Draw ACK/NACK mark for our own messages
|
// Draw ACK/NACK mark for our own messages
|
||||||
@@ -664,7 +717,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
if (isMine[i]) {
|
if (isMine[i]) {
|
||||||
// Calculate actual rendered width including emotes
|
// Calculate actual rendered width including emotes
|
||||||
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||||
int rightX = SCREEN_WIDTH - renderedWidth - 2; // -2 for slight padding from the edge
|
constexpr int SCROLLBAR_WIDTH = 3;
|
||||||
|
int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - 2;
|
||||||
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
|
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
|
||||||
} else {
|
} else {
|
||||||
drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes);
|
drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes);
|
||||||
@@ -672,7 +726,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
int totalContentHeight = totalHeight;
|
||||||
|
int visibleHeight = usableHeight;
|
||||||
|
|
||||||
|
// Draw scrollbar
|
||||||
|
drawMessageScrollbar(display, visibleHeight, totalContentHeight, finalScroll, getTextPositions(display)[1]);
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
graphics::drawCommonFooter(display, x, y);
|
graphics::drawCommonFooter(display, x, y);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
|||||||
// Clear Message Line Cache from Message Renderer
|
// Clear Message Line Cache from Message Renderer
|
||||||
void clearMessageCache();
|
void clearMessageCache();
|
||||||
|
|
||||||
|
void scrollUp();
|
||||||
|
void scrollDown();
|
||||||
|
|
||||||
|
// Determines if a line is a header line
|
||||||
|
bool isHeader(const std::string &line);
|
||||||
|
|
||||||
} // namespace MessageRenderer
|
} // namespace MessageRenderer
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
#endif
|
#endif
|
||||||
Reference in New Issue
Block a user