feat(chat): side-by-side settings modal with live conversation preview
Widen the chat settings modal and split it into a live message preview (canned bubbles rendered with the real style/accent/density/font/emoji/timestamp code) beside the controls; size the Done button to its text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -191,6 +191,121 @@ std::string initialOf(const std::string& name) {
|
||||
return name.substr(i, std::min(len, name.size() - i));
|
||||
}
|
||||
|
||||
// Live "conversation preview" for the chat settings modal. Renders a few canned bubbles with the SAME
|
||||
// style / accent / fill / rounding / density / font-size / timestamp / emoji code the real message loop
|
||||
// uses (see the loop in RenderChatTab), so the modal shows exactly what chat will look like. Purely
|
||||
// presentational — no hover/click handlers. Reserves a `width`-wide, content-tall block via a Dummy.
|
||||
void RenderChatSettingsPreview(App* app, float width) {
|
||||
auto* cs = app ? app->settings() : nullptr;
|
||||
if (!cs) return;
|
||||
const float dp = Layout::dpiScale();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// ---- Settings-derived draw params (mirror the real loop's formulas exactly). ----
|
||||
const int bubbleStyle = cs->getChatBubbleStyle(); // 0 rounded, 1 square, 2 minimal
|
||||
const bool compact = cs->getChatDensity() == 1;
|
||||
const float fscale = cs->getChatFontScale();
|
||||
ImFont* nameFont = material::Type().body2();
|
||||
ImFont* metaFont = material::Type().caption();
|
||||
const float nameSz = scaledSize(nameFont);
|
||||
const float metaSz = scaledSize(metaFont);
|
||||
const float bodySz = nameSz * (18.0f / std::max(1.0f, Layout::kFontBody2())) * fscale;
|
||||
const float bpad = (compact ? 6.0f : 9.0f) * dp;
|
||||
const float bround = (bubbleStyle == 1 ? 2.0f : 9.0f) * dp;
|
||||
const int outFill = (bubbleStyle == 2 ? 26 : 46);
|
||||
const int inFill = (bubbleStyle == 2 ? 14 : 22);
|
||||
const float groupGap = (compact ? 4.0f : 7.0f) * dp;
|
||||
const float msgGap = (compact ? 2.0f : 3.0f) * dp;
|
||||
const ImU32 accentBase = bubbleAccentColor(cs->getChatBubbleAccent());
|
||||
const float avR = 11.0f * dp;
|
||||
const float gutter = 2.0f * avR + 6.0f * dp;
|
||||
|
||||
// Resolve the chat clock the same way line ~379 does, but locally so the preview reflects the
|
||||
// timestamp control the SAME frame it changes (0 = follow global, 1 = 24h, 2 = 12h).
|
||||
const int ctf = cs->getChatTimeFormat();
|
||||
const bool prev12h = (ctf == 2) || (ctf == 0 && cs->getTimeFormat() == 1);
|
||||
const char* t1 = prev12h ? "9:41 AM" : "09:41";
|
||||
const char* t2 = prev12h ? "9:42 AM" : "09:42";
|
||||
|
||||
// Canned conversation: one incoming bubble + a grouped outgoing pair (to show the flattened corner
|
||||
// of the rounded/minimal styles). Emoji in the bodies show the mono/color emoji style.
|
||||
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
|
||||
const std::string peer = "Ava";
|
||||
const PMsg msgs[] = {
|
||||
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 },
|
||||
{ u8"Yep — just confirmed ✅", true, true, false, std::string(TR("chat_you")) + " " + t2 },
|
||||
{ u8"Sending the rest now \U0001F44D", true, false, true, std::string() },
|
||||
};
|
||||
const int N = 3;
|
||||
|
||||
// Panel geometry + per-message layout (pre-measured so the glass panel is drawn behind at the exact
|
||||
// height, since the draw list has no retained ordering).
|
||||
const float padIn = 10.0f * dp;
|
||||
const float contentW = std::max(80.0f * dp, width - 2.0f * padIn);
|
||||
const float maxBubbleW = std::max(120.0f * dp, contentW * 0.80f);
|
||||
const float innerW = maxBubbleW - 2.0f * bpad;
|
||||
struct PLay { ImVec2 tsz; float bw; float bh; };
|
||||
PLay lay[3];
|
||||
float totalH = padIn;
|
||||
for (int i = 0; i < N; ++i) {
|
||||
if (msgs[i].startGroup) totalH += (i > 0 ? groupGap : 0.0f) + metaSz + 3.0f * dp;
|
||||
else totalH += msgGap;
|
||||
lay[i].tsz = nameFont->CalcTextSizeA(bodySz, innerW, innerW, msgs[i].body);
|
||||
lay[i].bw = std::min(maxBubbleW, lay[i].tsz.x + 2.0f * bpad);
|
||||
lay[i].bh = lay[i].tsz.y + 2.0f * bpad;
|
||||
totalH += lay[i].bh;
|
||||
}
|
||||
totalH += padIn;
|
||||
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
material::GlassPanelSpec g; g.rounding = 10.0f * dp; g.fillAlpha = 16; g.borderAlpha = 36;
|
||||
material::DrawGlassPanel(dl, origin, ImVec2(origin.x + width, origin.y + totalH), g);
|
||||
|
||||
const float leftX = origin.x + padIn;
|
||||
const float rowW = contentW;
|
||||
float cy = origin.y + padIn;
|
||||
for (int i = 0; i < N; ++i) {
|
||||
const bool outgoing = msgs[i].outgoing;
|
||||
if (msgs[i].startGroup) {
|
||||
if (i > 0) cy += groupGap;
|
||||
const ImVec2 msz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, msgs[i].meta.c_str());
|
||||
const float mtx = outgoing ? (leftX + rowW - msz.x) : (leftX + gutter);
|
||||
dl->AddText(metaFont, metaSz, ImVec2(mtx, cy), material::OnSurfaceMedium(), msgs[i].meta.c_str());
|
||||
cy += metaSz + 3.0f * dp;
|
||||
} else {
|
||||
cy += msgGap;
|
||||
}
|
||||
const float bw = lay[i].bw, bh = lay[i].bh;
|
||||
const float bx = outgoing ? (leftX + rowW - bw) : (leftX + gutter);
|
||||
const ImVec2 bmin(bx, cy), bmax(bx + bw, cy + bh);
|
||||
const ImU32 bubCol = outgoing ? material::WithAlpha(accentBase, outFill)
|
||||
: material::WithAlpha(material::OnSurface(), inFill);
|
||||
ImDrawFlags rf = ImDrawFlags_RoundCornersAll;
|
||||
if (bubbleStyle != 1) { // square keeps uniform corners; rounded/minimal flatten stacked corners
|
||||
if (outgoing) {
|
||||
if (!msgs[i].startGroup) rf &= ~ImDrawFlags_RoundCornersTopRight;
|
||||
if (!msgs[i].lastInGroup) rf &= ~ImDrawFlags_RoundCornersBottomRight;
|
||||
} else {
|
||||
if (!msgs[i].startGroup) rf &= ~ImDrawFlags_RoundCornersTopLeft;
|
||||
if (!msgs[i].lastInGroup) rf &= ~ImDrawFlags_RoundCornersBottomLeft;
|
||||
}
|
||||
}
|
||||
dl->AddRectFilled(bmin, bmax, bubCol, bround, rf);
|
||||
dl->AddText(nameFont, bodySz, ImVec2(bx + bpad, cy + bpad), material::OnSurface(),
|
||||
msgs[i].body, nullptr, innerW);
|
||||
if (!outgoing && msgs[i].lastInGroup) { // peer avatar beside the last incoming bubble of the run
|
||||
const ImVec2 avc(leftX + avR, cy + bh - avR);
|
||||
dl->AddCircleFilled(avc, avR, avatarColor("preview:" + peer), 20);
|
||||
const std::string init = initialOf(peer);
|
||||
const ImVec2 isz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, init.c_str());
|
||||
dl->AddText(metaFont, metaSz, ImVec2(avc.x - isz.x * 0.5f, avc.y - isz.y * 0.5f),
|
||||
IM_COL32(255, 255, 255, 235), init.c_str());
|
||||
}
|
||||
cy += bh;
|
||||
}
|
||||
ImGui::Dummy(ImVec2(width, totalH));
|
||||
}
|
||||
|
||||
// Render a peer identity key (hex) as a groupable fingerprint for the verify tooltip: uppercased,
|
||||
// spaced every 4 hex chars, wrapped every 16 (8 bytes) so two users can read it off line by line.
|
||||
std::string keyFingerprint(const std::string& hex) {
|
||||
@@ -1195,16 +1310,43 @@ void RenderChatTab(App* app)
|
||||
ov.title = TR("chat_settings_title");
|
||||
ov.p_open = &s_show_chat_settings; // X / backdrop closes it
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 520.0f; ov.idSuffix = "chatsettings";
|
||||
// Wider than tall: a live preview sits to the LEFT of the controls (more horizontal room to work
|
||||
// with than vertical). Both columns AutoResizeY so neither the preview nor the controls clip.
|
||||
ov.cardWidth = 800.0f; ov.idSuffix = "chatsettings";
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
RenderChatSettingsControls(app);
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * Layout::dpiScale()));
|
||||
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)));
|
||||
if (material::TactileButton(TR("chat_settings_done"), ImVec2(ImGui::GetContentRegionAvail().x, 0)))
|
||||
s_show_chat_settings = false;
|
||||
ImGui::PopStyleColor(3);
|
||||
const float dp = Layout::dpiScale();
|
||||
const float colGap = 16.0f * dp;
|
||||
const float fullW = ImGui::GetContentRegionAvail().x;
|
||||
const float previewW = std::min(340.0f * dp, std::max(240.0f * dp, fullW * 0.40f));
|
||||
const float controlsW = std::max(300.0f * dp, fullW - previewW - colGap);
|
||||
|
||||
ImGui::BeginChild("##chatPreviewCol", ImVec2(previewW, 0.0f), ImGuiChildFlags_AutoResizeY,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
RenderChatSettingsPreview(app, ImGui::GetContentRegionAvail().x);
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine(0.0f, colGap);
|
||||
|
||||
ImGui::BeginChild("##chatControlsCol", ImVec2(controlsW, 0.0f), ImGuiChildFlags_AutoResizeY,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
RenderChatSettingsControls(app, 0.0f);
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
|
||||
{
|
||||
// Done: sized to its text (+ a little shoulder room) and centered, not full width.
|
||||
const char* doneLbl = TR("chat_settings_done");
|
||||
ImGui::PushFont(material::Type().button());
|
||||
const float bw = ImGui::CalcTextSize(doneLbl).x + ImGui::GetStyle().FramePadding.x * 2.0f + 28.0f * dp;
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
|
||||
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)));
|
||||
if (material::TactileButton(doneLbl, ImVec2(bw, 0)))
|
||||
s_show_chat_settings = false;
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user