feat(chat): redesign message composer — radial byte gauge, autogrow, wrap
Replace the plain "N/236" byte counter with a radial wheel drawn inside the input, right-aligned, filling green→amber→red as the message approaches the 236-byte memo cap. Vertically center the text with left padding (FramePadding), hard-cap typing at the cap (no silent overflow), add Shift+Enter for a newline (via an always-callback, since ImGui 1.92's Enter shortcut needs exact mods) while plain Enter still sends, animate the input growing taller as lines are added, and word-wrap the display instead of overflowing left. Give a hard newline a touch more space above than a soft wrap in the message bubble. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <cmath> // std::cos/std::sin — radial byte gauge in the composer
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
@@ -44,6 +45,25 @@ std::string s_scroll_to_cid; // when set, scroll the thread to the bottom next
|
||||
// Composer + new-conversation UI state.
|
||||
char s_compose[512] = "";
|
||||
std::string s_compose_cid; // the conversation s_compose is a draft for; draft is wiped when it changes
|
||||
// On-chain chat body cap in bytes = (512 − len("utf8:"))/2 − secretstream ABYTES (see chat_outgoing.cpp).
|
||||
// The composer hard-caps input to this; the emoji picker respects it too.
|
||||
constexpr int kChatBodyMaxBytes = (512 - 5) / 2 - 17; // = 236
|
||||
float s_composerAnimH = 0.0f; // animated composer-box height — grows with newlines, pushing the thread up
|
||||
float s_composerTargetH = 0.0f; // target height measured in the composer block (wrapped content), consumed next frame
|
||||
|
||||
// Composer InputText callback: insert a newline at the cursor on Shift+Enter. ImGui 1.92's Enter handling
|
||||
// (imgui_widgets.cpp:5078) resolves Enter via Shortcut() with EXACT modifiers, so Shift+Enter matches
|
||||
// neither the plain-Enter (submit) nor the Ctrl+Enter shortcut — ImGui does nothing with it. We insert the
|
||||
// newline ourselves here (running under CallbackAlways), respecting the on-chain byte cap.
|
||||
int composeInputCallback(ImGuiInputTextCallbackData* data) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (io.KeyShift
|
||||
&& (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter))
|
||||
&& data->BufTextLen < kChatBodyMaxBytes) {
|
||||
data->InsertChars(data->CursorPos, "\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
bool s_show_new_convo = false;
|
||||
char s_new_zaddr[128] = "";
|
||||
char s_new_msg[256] = "";
|
||||
@@ -165,6 +185,35 @@ ImU32 avatarColor(const std::string& seed) {
|
||||
return kPalette[h % (sizeof(kPalette) / sizeof(kPalette[0]))];
|
||||
}
|
||||
|
||||
// Lay out (and optionally draw) a chat message body with paragraph spacing: each '\n'-delimited paragraph
|
||||
// word-wraps at wrapW, and every paragraph after the first gets `paraGap` of extra leading ABOVE it — so
|
||||
// an explicit newline reads as a paragraph break (a bit more space than a soft wrap, which stays tight).
|
||||
// Returns the total laid-out size (max wrapped width × total height). When dl != nullptr, draws at `origin`.
|
||||
// Both the size pass (dl == nullptr) and the draw pass MUST use identical args so the bubble fits the text.
|
||||
ImVec2 layoutChatBody(ImDrawList* dl, ImFont* font, float size, const std::string& body,
|
||||
float wrapW, float paraGap, ImVec2 origin, ImU32 col) {
|
||||
float maxW = 0.0f, y = 0.0f;
|
||||
bool first = true;
|
||||
size_t start = 0;
|
||||
for (;;) {
|
||||
const size_t nl = body.find('\n', start);
|
||||
const bool last = (nl == std::string::npos);
|
||||
const char* pbeg = body.c_str() + start;
|
||||
const char* pend = body.c_str() + (last ? body.size() : nl);
|
||||
if (!first) y += paraGap; // paragraph break gets extra room
|
||||
const ImVec2 psz = font->CalcTextSizeA(size, FLT_MAX, wrapW, pbeg, pend);
|
||||
const float ph = std::max(psz.y, size); // a blank line still takes one line
|
||||
if (dl && pend > pbeg)
|
||||
dl->AddText(font, size, ImVec2(origin.x, origin.y + y), col, pbeg, pend, wrapW);
|
||||
y += ph;
|
||||
maxW = std::max(maxW, psz.x);
|
||||
first = false;
|
||||
if (last) break;
|
||||
start = nl + 1;
|
||||
}
|
||||
return ImVec2(maxW, y);
|
||||
}
|
||||
|
||||
// Outgoing-bubble accent base color for a chat_bubble_accent preset (0 = theme primary).
|
||||
ImU32 bubbleAccentColor(int accent) {
|
||||
switch (accent) {
|
||||
@@ -465,7 +514,11 @@ void renderEmojiPickerOverlay(char* buf, std::size_t bufSize) {
|
||||
ImGui::PushID(shown);
|
||||
if (material::IconButton("##em", e.glyph, emojiFont, ImVec2(cell, cell), es)) {
|
||||
const std::size_t cur = std::strlen(buf), add = std::strlen(e.glyph);
|
||||
if (cur + add < bufSize) { std::memcpy(buf + cur, e.glyph, add); buf[cur + add] = '\0'; }
|
||||
// Respect the on-chain byte cap (not just the raw buffer) so the emoji path can't
|
||||
// overshoot what the input's own hard cap allows.
|
||||
if (cur + add <= static_cast<std::size_t>(kChatBodyMaxBytes) && cur + add < bufSize) {
|
||||
std::memcpy(buf + cur, e.glyph, add); buf[cur + add] = '\0';
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
if (++shown % perRow != 0) ImGui::SameLine();
|
||||
@@ -534,6 +587,7 @@ void RenderChatTab(App* app)
|
||||
if (s_selected_cid != s_compose_cid) {
|
||||
sodium_memzero(s_compose, sizeof(s_compose));
|
||||
s_compose_cid = s_selected_cid;
|
||||
s_composerAnimH = 0.0f; // re-arm the first-frame snap so the box doesn't animate-collapse on switch
|
||||
}
|
||||
// Abandon an in-progress header rename tied to a different conversation (switch/hide), so a
|
||||
// half-typed buffer can't silently reappear or land on the wrong contact (Tier C review).
|
||||
@@ -705,11 +759,28 @@ void RenderChatTab(App* app)
|
||||
for (const auto& c : convs) if (c.cid == s_selected_cid) { sel = &c; break; }
|
||||
|
||||
const float tdp = Layout::dpiScale();
|
||||
const float composerBoxH = ImGui::GetTextLineHeight() * 2.6f; // multi-line composer (Q6)
|
||||
// Reserve a stable composer strip below the box whenever a conversation is open (thin counter row +
|
||||
// the input row, which sits flush with the strip/window bottom). Same height for the waiting hint so
|
||||
// the box doesn't jump when a reply arrives.
|
||||
const float composerAreaH = sel ? (composerBoxH + metaSz + 12.0f * tdp) : 0.0f;
|
||||
// Composer auto-grows with its content: the input word-wraps, so the box height tracks the wrapped
|
||||
// line count (measured in the composer block below, where the wrap width is known) and animates so the
|
||||
// thread above is smoothly pushed up/down. The vertical padding also centers a single line and, with
|
||||
// the left FramePadding, keeps text off the edges. We consume last frame's measured target here (a
|
||||
// 1-frame lag that's invisible under the smoothing); it falls back to one collapsed line.
|
||||
const float lineH = ImGui::GetTextLineHeight();
|
||||
const float composerVPad = 8.0f * tdp; // top+bottom inner padding
|
||||
const float composerCtrlH = lineH + 2.0f * composerVPad; // one-line (collapsed) height; side controls match
|
||||
const int kMaxComposerLines = 6; // beyond this the input scrolls internally
|
||||
const float composerAnimTarget = (s_composerTargetH > 0.0f) ? s_composerTargetH : composerCtrlH;
|
||||
if (s_composerAnimH <= 0.0f) s_composerAnimH = composerAnimTarget; // first frame: snap, don't animate in
|
||||
{
|
||||
const float dt = ImGui::GetIO().DeltaTime;
|
||||
const float k = 1.0f - std::exp(-dt * 16.0f); // ~16/s exponential convergence
|
||||
s_composerAnimH += (composerAnimTarget - s_composerAnimH) * k;
|
||||
if (std::fabs(composerAnimTarget - s_composerAnimH) < 0.5f) s_composerAnimH = composerAnimTarget;
|
||||
}
|
||||
const float composerBoxH = s_composerAnimH;
|
||||
// Reserve the (animated) composer strip below the box whenever a conversation is open — just the input
|
||||
// row (the byte budget is a radial gauge inside the input, no separate counter row). Same height for
|
||||
// the waiting hint so the box doesn't jump when a reply arrives.
|
||||
const float composerAreaH = sel ? (composerBoxH + 10.0f * tdp) : 0.0f;
|
||||
|
||||
// Frosted-glass thread pane (same reasoning as the list): glass behind + transparent child bg so
|
||||
// the message area blurs the backdrop instead of showing the sharp texture. Replaces the old
|
||||
@@ -1066,7 +1137,10 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
|
||||
// ── Direction-aligned bubble (rounding/fill/accent from settings; grouped corners).
|
||||
const ImVec2 tsz = nameFont->CalcTextSizeA(bodySz, innerW, innerW, m.body.c_str());
|
||||
// Body is laid out paragraph-by-paragraph so explicit newlines get extra leading.
|
||||
const float paraGap = bodySz * 0.35f;
|
||||
const ImVec2 tsz = layoutChatBody(nullptr, nameFont, bodySz, m.body, innerW, paraGap,
|
||||
ImVec2(0, 0), 0);
|
||||
const float bw = std::min(maxBubbleW, tsz.x + 2.0f * bpad);
|
||||
const float bh = tsz.y + 2.0f * bpad;
|
||||
const ImVec2 cur = ImGui::GetCursorScreenPos();
|
||||
@@ -1087,8 +1161,8 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
}
|
||||
dl->AddRectFilled(bmin, bmax, bubCol, bround, rf);
|
||||
dl->AddText(nameFont, bodySz, ImVec2(bx + bpad, cur.y + bpad), material::OnSurface(),
|
||||
m.body.c_str(), nullptr, innerW);
|
||||
layoutChatBody(dl, nameFont, bodySz, m.body, innerW, paraGap,
|
||||
ImVec2(bx + bpad, cur.y + bpad), material::OnSurface());
|
||||
|
||||
// Peer avatar beside the LAST incoming bubble of a run (Tier 3).
|
||||
if (!outgoing && lastInGroup) {
|
||||
@@ -1188,6 +1262,7 @@ void RenderChatTab(App* app)
|
||||
const float cx = boxMin.x;
|
||||
const float cw = boxMax.x - boxMin.x;
|
||||
if (sel->peerPubKey.empty()) {
|
||||
s_composerTargetH = composerCtrlH; // no editable composer here — keep the strip collapsed
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, boxMax.y + 8.0f * tdp));
|
||||
ImGui::PushFont(metaFont);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
@@ -1197,31 +1272,36 @@ void RenderChatTab(App* app)
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
// Multi-line composer (Q6). Enter behavior is setting-dependent (see below). A live byte
|
||||
// counter tracks the on-chain body cap = (512 − len("utf8:"))/2 − secretstream ABYTES.
|
||||
static constexpr int kBodyMaxBytes = (512 - 5) / 2 - 17; // = 236 (see chat_outgoing.cpp)
|
||||
// Multi-line composer (Q6). Enter behavior is setting-dependent (see below). Input is
|
||||
// hard-capped to the on-chain body byte budget; usage is shown as a radial gauge in the box.
|
||||
const int kBodyMaxBytes = kChatBodyMaxBytes;
|
||||
const float sendW = 80.0f * tdp; // scaled so the translated label never clips (B3)
|
||||
const float emojiBtn = 28.0f * tdp;
|
||||
const float inGap = 6.0f * tdp;
|
||||
const int used = static_cast<int>(std::strlen(s_compose));
|
||||
const bool overCap = used > kBodyMaxBytes;
|
||||
const bool overCap = used > kBodyMaxBytes; // defensive — the hard byte cap should prevent this
|
||||
|
||||
// Thin counter row (right-aligned) just under the box; over-cap tints + prefixes a label.
|
||||
std::string counterLine = std::to_string(used) + " / " + std::to_string(kBodyMaxBytes);
|
||||
if (overCap) counterLine = std::string(TR("chat_len_over")) + " " + counterLine;
|
||||
const float cwid = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, counterLine.c_str()).x;
|
||||
ImGui::GetWindowDrawList()->AddText(metaFont, metaSz,
|
||||
ImVec2(cx + cw - cwid, boxMax.y + 3.0f * tdp),
|
||||
overCap ? material::Error() : material::OnSurfaceMedium(), counterLine.c_str());
|
||||
// Fraction of the on-chain cap used, for the radial gauge (drawn inside the input, below).
|
||||
const float frac = kBodyMaxBytes > 0 ? static_cast<float>(used) / kBodyMaxBytes : 0.0f;
|
||||
|
||||
// Input row, flush with the strip/window bottom: [emoji] [input] [Send].
|
||||
// Input row: [emoji] [input] [Send]. The input's TOP is pinned 10px below the message box and
|
||||
// it grows DOWNWARD as it gets taller (the thread above shrinks), so the row bottom stays put.
|
||||
const float rowY = boxMax.y + composerAreaH - composerBoxH;
|
||||
const float inputX = cx + emojiBtn + inGap;
|
||||
const float inputW = std::max(60.0f * tdp, cw - emojiBtn - 2.0f * inGap - sendW);
|
||||
const float ctrlY = rowY + composerBoxH - composerCtrlH; // bottom-aligned row for side controls
|
||||
// Radial byte gauge sits in the input box's bottom-right corner; a fixed small size (tied to
|
||||
// the line height, NOT the animated box height) with a reserved right slot so text never
|
||||
// reaches it. Geometry first so the input width floor can guarantee room for both.
|
||||
const float ringR = std::max(7.0f, lineH * 0.42f);
|
||||
const float ringPad = 9.0f * tdp;
|
||||
const float ringSlot = 2.0f * ringR + ringPad * 1.6f;
|
||||
const float inputW = std::max(ringSlot + 48.0f * tdp, cw - emojiBtn - 2.0f * inGap - sendW);
|
||||
const float sendX = inputX + inputW + inGap;
|
||||
const float textW = std::max(40.0f * tdp, inputW - ringSlot); // input area, left of the ring
|
||||
const ImVec2 ringC(inputX + inputW - ringPad - ringR, rowY + composerBoxH - ringPad - ringR);
|
||||
|
||||
// Emoji toggle on the left, vertically centered in the input row.
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, rowY + (composerBoxH - emojiBtn) * 0.5f));
|
||||
// Emoji toggle on the left, bottom-aligned with the input row (centered in the control height).
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, ctrlY + (composerCtrlH - emojiBtn) * 0.5f));
|
||||
{
|
||||
material::IconButtonStyle es;
|
||||
es.hoverBg = material::WithAlpha(material::OnSurface(), 30);
|
||||
@@ -1244,33 +1324,108 @@ void RenderChatTab(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, IM_COL32(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, IM_COL32(0, 0, 0, 0));
|
||||
// FramePadding gives the text a left inset (so it doesn't hug the box edge) and, since the
|
||||
// collapsed box is exactly one line + 2×VPad tall, vertically centers a single line.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f * tdp, composerVPad));
|
||||
ImGui::SetCursorScreenPos(ImVec2(inputX, rowY));
|
||||
// Enter-to-send (setting): on → Enter sends + Ctrl+Enter newline; off → Enter is a newline and
|
||||
// the Send button is the only way to send. NOTE: without EnterReturnsTrue, InputTextMultiline
|
||||
// returns true on EVERY edit (value-changed), so in off-mode the return value must NOT drive
|
||||
// submit — otherwise typing one character would fire a send.
|
||||
// Enter-to-send (setting): on → Enter sends, Shift+Enter makes a newline (Ctrl+Enter too); off
|
||||
// → Enter is a newline and the Send button is the only way to send. EnterReturnsTrue needs
|
||||
// CtrlEnterForNewLine alongside it or multiline plain-Enter inserts a '\n' instead of
|
||||
// validating (imgui_widgets.cpp:5118). Shift+Enter is handled in composeInputCallback since
|
||||
// ImGui's exact-modifier Enter shortcut ignores it. CallbackAlways runs that handler.
|
||||
const bool enterSends = !app->settings() || app->settings()->getChatEnterSends();
|
||||
const ImGuiInputTextFlags composeFlags = enterSends
|
||||
? (ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CtrlEnterForNewLine)
|
||||
: ImGuiInputTextFlags_None;
|
||||
const bool inputReturned = ImGui::InputTextMultiline("##compose", s_compose, sizeof(s_compose),
|
||||
ImVec2(inputW, composerBoxH), composeFlags);
|
||||
// WordWrap: long lines wrap to the next visual line (the box grows) instead of scrolling the
|
||||
// text horizontally off the left edge.
|
||||
ImGuiInputTextFlags composeFlags = ImGuiInputTextFlags_CallbackAlways | ImGuiInputTextFlags_WordWrap;
|
||||
if (enterSends)
|
||||
composeFlags |= ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CtrlEnterForNewLine;
|
||||
// Hard-cap the byte length at the on-chain budget: passing kBodyMaxBytes+1 as the buffer size
|
||||
// stops ImGui from accepting more input (UTF-8-safe), so the user can't type past the cap.
|
||||
const bool inputReturned = ImGui::InputTextMultiline("##compose", s_compose, kBodyMaxBytes + 1,
|
||||
ImVec2(textW, composerBoxH), composeFlags, composeInputCallback);
|
||||
ImGui::PopStyleVar(); // FramePadding
|
||||
ImGui::PopStyleColor(3); // composer FrameBg (transparent — glass shows through)
|
||||
bool submit = enterSends && inputReturned; // off-mode sends only via the Send button
|
||||
// Send.
|
||||
ImGui::SetCursorScreenPos(ImVec2(sendX, rowY));
|
||||
|
||||
// Measure the wrapped content height to drive the auto-grow (consumed next frame at the top).
|
||||
// Wrap width mirrors ImGui's internal InputText wrap = frame inner width (textW − 2×FramePadding.x),
|
||||
// so CalcTextSize's wrap matches what the widget renders.
|
||||
{
|
||||
const float wrapW = std::max(1.0f, textW - 2.0f * 12.0f * tdp);
|
||||
float contentH = (s_compose[0] == '\0') ? lineH
|
||||
: ImGui::CalcTextSize(s_compose, nullptr, false, wrapW).y;
|
||||
const size_t clen = std::strlen(s_compose);
|
||||
if (clen > 0 && s_compose[clen - 1] == '\n') contentH += lineH; // trailing empty line
|
||||
contentH = std::min(contentH, kMaxComposerLines * lineH);
|
||||
s_composerTargetH = std::max(lineH, contentH) + 2.0f * composerVPad;
|
||||
}
|
||||
|
||||
// ── Radial byte gauge (replaces the old numeric counter): a ring in the input's right edge
|
||||
// that fills clockwise and shifts green→amber→red as the message nears the on-chain cap.
|
||||
// Full + red at/over the cap (Send is disabled there); exact count shows on hover.
|
||||
{
|
||||
ImDrawList* gdl = ImGui::GetWindowDrawList();
|
||||
auto lerpCol = [](ImU32 a, ImU32 b, float t) {
|
||||
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
|
||||
const ImVec4 fa = ImGui::ColorConvertU32ToFloat4(a);
|
||||
const ImVec4 fb = ImGui::ColorConvertU32ToFloat4(b);
|
||||
return ImGui::GetColorU32(ImVec4(fa.x + (fb.x - fa.x) * t, fa.y + (fb.y - fa.y) * t,
|
||||
fa.z + (fb.z - fa.z) * t, fa.w + (fb.w - fa.w) * t));
|
||||
};
|
||||
const ImU32 green = material::Success(), amber = material::Warning(), red = material::Error();
|
||||
ImU32 fillCol;
|
||||
if (frac < 0.75f) fillCol = green; // calm until genuinely near the cap
|
||||
else if (frac < 1.0f) { const float t = (frac - 0.75f) / 0.25f;
|
||||
fillCol = (t < 0.5f) ? lerpCol(green, amber, t / 0.5f)
|
||||
: lerpCol(amber, red, (t - 0.5f) / 0.5f); }
|
||||
else fillCol = red; // at/over cap
|
||||
|
||||
const float thick = std::max(2.0f, 2.4f * tdp);
|
||||
const float dispFrac = frac < 0.0f ? 0.0f : (frac > 1.0f ? 1.0f : frac);
|
||||
// Faint full-ring track (alpha kept high enough to read on light skins too).
|
||||
gdl->PathArcTo(ringC, ringR, 0.0f, IM_PI * 2.0f, 48);
|
||||
gdl->PathStroke(material::WithAlpha(material::OnSurface(), 42), 0, thick);
|
||||
// Progress arc from 12 o'clock, clockwise, with a rounded head.
|
||||
if (dispFrac > 0.0001f) {
|
||||
const float a0 = -IM_PI * 0.5f;
|
||||
const float a1 = a0 + dispFrac * IM_PI * 2.0f;
|
||||
gdl->PathArcTo(ringC, ringR, a0, a1, 48);
|
||||
gdl->PathStroke(fillCol, 0, thick);
|
||||
gdl->AddCircleFilled(ImVec2(ringC.x + std::cos(a1) * ringR, ringC.y + std::sin(a1) * ringR),
|
||||
thick * 0.6f, fillCol, 10);
|
||||
}
|
||||
// Exact count on hover (progressive disclosure; over-cap prefixes a label).
|
||||
const float hitR = ringR + thick;
|
||||
if (ImGui::IsMouseHoveringRect(ImVec2(ringC.x - hitR, ringC.y - hitR),
|
||||
ImVec2(ringC.x + hitR, ringC.y + hitR))) {
|
||||
std::string tip = std::to_string(used) + " / " + std::to_string(kBodyMaxBytes);
|
||||
if (overCap) tip = std::string(TR("chat_len_over")) + " " + tip;
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(tip.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
// Off-mode sends only via the Send button. In enter-sends mode EnterReturnsTrue makes the
|
||||
// return value mean "validated" (plain Enter only) — Shift+Enter/edits don't set it.
|
||||
bool submit = enterSends && inputReturned;
|
||||
// Send — fixed height, bottom-aligned with the input row (doesn't stretch when the box grows).
|
||||
ImGui::SetCursorScreenPos(ImVec2(sendX, ctrlY));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
|
||||
ImGui::BeginDisabled(overCap || s_compose[0] == '\0');
|
||||
if (material::TactileButton(TR("chat_send"), ImVec2(sendW, composerBoxH))) submit = true;
|
||||
if (material::TactileButton(TR("chat_send"), ImVec2(sendW, composerCtrlH))) submit = true;
|
||||
ImGui::EndDisabled();
|
||||
// When Send is disabled *because the message is over the cap*, explain why on hover — the
|
||||
// gauge went red but the numeric reason is no longer always-visible.
|
||||
if (overCap && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetTooltip("%s %d / %d", TR("chat_len_over"), used, kBodyMaxBytes);
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
if (submit && s_compose[0] != '\0' && !overCap) {
|
||||
app->sendChatMessage(sel->cid, s_compose);
|
||||
sodium_memzero(s_compose, sizeof(s_compose));
|
||||
s_scroll_to_cid = sel->cid;
|
||||
s_composerAnimH = 0.0f; // snap back to collapsed instead of animating while unfocused
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user