Compare commits

...
Author SHA1 Message Date
ExPikaPaka 016b1f6a32 Give the sliders a typed value without touching a single call site
float_row() and int_row() keep the signature they had. The edit icon is
drawn first, at the right end of the row, and the slider after it, so the
slider is still the last item submitted and the hover_tip() every caller
already writes after the row still describes the slider rather than the
icon. The range note moves onto the icon's own tooltip, which this helper
owns, instead of being threaded through every row as an extra argument.

That leaves the forty-odd rows in the panel byte for byte as they were,
which is what lets the branches that edit some of those rows - the model-UV
one above all - merge in any order. The two members and the edit icon also
move off lines a neighbouring branch inserts at.
2026-10-12 14:08:12 +02:00
ExPikaPaka ed1f3e52c9 Texture displacement: typed values on every panel slider
Every slider in the texture displacement panel now takes an exact number,
either by Ctrl+clicking the track or through a small edit icon at its right
end. Both routes end in ImGui's own temp-input mode: the icon only asks ImGui
to focus the slider, which is what turns it into an input box, so the parsing,
the clamping and the Enter/click-away behaviour stay ImGui's rather than
becoming a second implementation.

The mechanism lives in one helper, typed_row(), with a float and an int form
over it. float_row() and int_row() are thin wrappers on those, and the six
sliders that previously built their widget inline (brush size, smart fill
angle, edge fade amount, remesh sharp angle, refine resolution and the uniform
triangle budget) now go through the same path.

Each slider carries a tooltip with its description and its range. The range is
derived from the bounds and the format string the slider is already given, so
no row states them twice and the tooltip cannot drift from the track. The
tooltip allows hovering while disabled, since several rows are drawn inside
disabled_begin(), where a greyed slider is exactly when the explanation is
wanted.

The panel's "?" popup gains an entry for each of the two ways in, appended to
every tool's shortcut list and shown on their own when no painting tool is
selected, where that popup was previously empty.
2026-10-12 07:57:07 +02:00
2 changed files with 114 additions and 14 deletions
@@ -494,7 +494,17 @@ bool GLGizmoTextureDisplacement::on_init()
{ctrl + _L("Mouse wheel"), m_desc["gap_area"]},
toggle_wireframe_shortcut
};
// Both ways into the same input box on any slider in the panel: ImGui turns a slider into one when
// it is Ctrl+clicked or when it is given the keyboard focus, which is what the edit icon asks for.
m_desc["type_value"] = _L("Type an exact value into a slider");
m_shortcuts_sliders = {
{ctrl + _L("Left mouse button"), m_desc["type_value"]},
{_L("Edit icon at a slider's right end"), m_desc["type_value"]},
};
for (std::vector<std::pair<wxString, wxString>> *list : {&m_shortcuts_brush, &m_shortcuts_bucket_fill, &m_shortcuts_gap_fill})
list->insert(list->end(), m_shortcuts_sliders.begin(), m_shortcuts_sliders.end());
return true;
}
@@ -3680,6 +3690,7 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
"texture_displacement_adjust.svg", "canvas_drag.svg", "texture_displacement_move_up.svg",
"texture_displacement_move_down.svg", "texture_displacement_drag.svg",
"texture_displacement_select_all.svg", "texture_displacement_erase_all.svg",
"edit.svg", // the "type a value" button at the right end of every slider
};
std::vector<std::string> paths;
paths.reserve(names.size());
@@ -5919,8 +5930,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
}
return s + "...";
};
// Label column + a slider filling the rest of the row, value printed inside the track. Ctrl+click on
// any of these types a value in. `right_inset` keeps a layer card's inner padding.
// Label column + a slider filling the rest of the row, value printed inside the track.
// `right_inset` keeps a layer card's inner padding.
const auto slider_label = [&](const wxString &label) {
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
@@ -5928,17 +5939,98 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
};
// ---- Typed values on sliders ----
// Every slider in the panel goes through typed_row(), so all of them take a number the same two
// ways: Ctrl+click on the track, or the edit icon at the right end for the same box without the
// modifier. Both end up in ImGui's own "temp input" mode - the icon only asks ImGui to focus the
// slider, which is what turns it into an input box (see SliderScalar) - so the parsing, the
// clamping and the Enter/click-away behaviour are ImGui's, not a second implementation.
// Retires a click whose row is no longer being drawn - the panel changed mode in between - so that
// it cannot fire later when that row comes back. Before any row is drawn, so a live one still wins.
if (!m_slider_type_request.empty() && ImGui::GetFrameCount() > m_slider_type_request_frame + 1)
m_slider_type_request.clear();
// Exactly as tall as the slider it sits beside, so it lines up with the track without the row
// having to be re-centred around it; icon_button() pads the glyph itself down to about 0.7 of this.
const float edit_icon_w = frame_h;
// What the slider's own width has to give up for the icon beside it.
const float edit_icon_room = edit_icon_w + gap_s;
// One end of a range, through the slider's own format, so the bounds in the tooltip read exactly
// like the value in the track. A parenthesised note in the format - "(auto)" - is dropped: it
// describes the current state, not the bound.
const auto fmt_bound = [](const char *format, auto v) {
char buf[64];
std::snprintf(buf, sizeof(buf), format, v);
std::string out(buf);
const size_t paren = out.find('(');
if (paren != std::string::npos)
out.erase(paren);
while (!out.empty() && out.back() == ' ')
out.pop_back();
return out;
};
const auto edit_icon_tip = [](const std::string &lo, const std::string &hi) {
return _u8L("Type a value instead of dragging. Ctrl+click on the slider does the same.") + "\n\n" +
Slic3r::format(_u8L("Range: %1% to %2%"), lo, hi);
};
// Draws the edit icon at the right end of the row and then `slider` to its left - in that order on
// purpose. The slider is then the last item submitted, so the hover_tip() a caller writes after the
// row still describes the slider rather than the icon, and no row has to pass its tooltip through
// here. `item_w` follows SetNextItemWidth's convention (negative measures back from the right edge)
// and already has edit_icon_room taken out of it.
const auto typed_row = [&](const char *id, float item_w, const std::string &tip,
const std::function<bool()> &slider) -> bool {
const float x0 = ImGui::GetCursorPosX();
const float w = item_w > 0.f ? item_w : std::max(1.f, ImGui::GetContentRegionAvail().x + item_w);
ImGui::SetCursorPosX(x0 + w + gap_s);
if (icon_button(int(ImGui::GetID(id)), "edit.svg", edit_icon_w, _L("Edit"), from_u8(tip))) {
m_slider_type_request = id;
m_slider_type_request_frame = ImGui::GetFrameCount();
m_parent.set_as_dirty(); // the next frame is where the request above is turned into focus
}
// Back onto the same row and to its start, which is where the slider belongs.
ImGui::SameLine(0.f, 0.f);
ImGui::SetCursorPosX(x0);
// Deferred from the click a frame ago: SetKeyboardFocusHere() is a request for the *next*
// frame, so it has to be issued before the slider it names, not from the icon beside it.
if (m_slider_type_request == id) {
ImGui::SetKeyboardFocusHere();
m_slider_type_request.clear();
m_parent.set_as_dirty(); // the focus only lands on a frame that actually gets drawn
}
ImGui::SetNextItemWidth(w);
return slider();
};
// The two forms every slider in the panel is one of. They derive the icon's range note from the
// bounds they are already given, so no row states its bounds or its format twice.
const auto typed_slider = [&](const char *id, float item_w, float *v, float v_min, float v_max,
const char *format, bool log) -> bool {
return typed_row(id, item_w, edit_icon_tip(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
return ImGui::SliderFloat(id, v, v_min, v_max, format,
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
});
};
const auto typed_slider_int = [&](const char *id, float item_w, int *v, int v_min, int v_max,
const char *format) -> bool {
return typed_row(id, item_w, edit_icon_tip(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
});
};
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max, const char *format,
bool log, float right_inset) -> bool {
slider_label(label);
ImGui::SetNextItemWidth(-std::max(right_inset, 1.f));
return ImGui::SliderFloat(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
return typed_slider(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, log);
};
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, const char *format,
float right_inset) -> bool {
slider_label(label);
ImGui::SetNextItemWidth(-std::max(right_inset, 1.f));
return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
return typed_slider_int(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format);
};
const auto layer_name = [](const TextureDisplacementLayer &l) {
return l.name.empty() ? Slic3r::format(_u8L("Layer %1%"), l.slot + 1) : l.name;
@@ -6160,10 +6252,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
const auto centre_on_row = [&](float h) { ImGui::SetCursorPosY(row_y + std::round((icon_md - h) * 0.5f)); };
vsep(icon_md);
if (is_brush_mode) {
ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm)));
centre_on_row(frame_h);
ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm",
ImGuiSliderFlags_AlwaysClamp);
typed_slider("##cursor_radius",
std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm) - edit_icon_room),
&m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", false);
hover_tip(m_desc.at("cursor_size"));
vsep(icon_sm);
const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE;
@@ -6177,10 +6269,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
_L("Sphere - paints only within a ball around the point under the cursor")))
m_cursor_type = TriangleSelector::CursorType::SPHERE;
} else if (is_area_mode) {
ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX()));
centre_on_row(frame_h);
ImGui::SliderFloat("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°",
ImGuiSliderFlags_AlwaysClamp);
typed_slider("##smart_fill_angle", std::max(1.f, row_end - ImGui::GetCursorPosX() - edit_icon_room),
&m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false);
hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this"));
} else {
centre_on_row(frame_h);
@@ -7372,7 +7463,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
: m_tool_type == ToolType::SMART_FILL ? m_shortcuts_bucket_fill
: m_tool_type == ToolType::BRUSH ? m_shortcuts_brush
: m_tool_type == ToolType::GAP_FILL ? m_shortcuts_gap_fill
: std::vector<std::pair<wxString, wxString>>{};
: m_shortcuts_sliders; // the sliders are there whether or not a painting tool is
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f); // center vertically
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
@@ -1016,6 +1016,15 @@ private:
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
// The two ways every slider in the panel takes a typed value. They hold whatever tool is selected,
// so they are tacked onto each of the lists above and stand on their own when no tool is active.
std::vector<std::pair<wxString, wxString>> m_shortcuts_sliders;
// The slider whose edit icon was just clicked, as its ImGui id, and the frame it was clicked on.
// The row acts on it on the next frame - ImGui's keyboard-focus request only takes effect a frame
// later - and clears it; the frame number is what retires a request whose row has gone off screen
// in the meantime. Panel state only, not saved with the project.
std::string m_slider_type_request;
int m_slider_type_request_frame = 0;
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in