feat(dialogs): add input and color pickers

This commit is contained in:
2026-06-18 21:00:51 -05:00
parent 80c6bfce90
commit 42ba806631
5 changed files with 266 additions and 2 deletions

View File

@@ -31,7 +31,7 @@ set_target_properties(izo PROPERTIES
if(WIN32)
target_compile_definitions(izo PRIVATE UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
target_link_libraries(izo PRIVATE ole32 shell32 uuid user32)
target_link_libraries(izo PRIVATE comdlg32 ole32 shell32 uuid user32)
elseif(UNIX AND NOT APPLE)
# No link-time desktop dependency. The Linux backend discovers zenity or
# kdialog at runtime so applications do not inherit GTK or Qt dependencies.

View File

@@ -1,5 +1,6 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
@@ -17,6 +18,59 @@ struct MessageBoxOptions {
};
MessageResponse ShowMessageBox(const MessageBoxOptions& options,
std::string* errorMessage = nullptr);
std::string* errorMessage = nullptr);
MessageResponse ShowMessageBox(std::string_view title, std::string_view message,
MessageIcon icon = MessageIcon::Info,
MessageButtons buttons = MessageButtons::Ok,
std::string* errorMessage = nullptr);
// Convenience wrappers around ShowMessageBox.
MessageResponse ShowErrorBox(std::string_view title, std::string_view message,
std::string* errorMessage = nullptr);
MessageResponse ShowQuestionBox(std::string_view title, std::string_view message,
std::string* errorMessage = nullptr);
struct InputBoxOptions {
std::string title;
std::string message;
std::string initialValue;
bool password = false;
};
struct InputBoxResult {
MessageResponse response = MessageResponse::Cancel;
std::string value;
std::string errorMessage;
explicit operator bool() const noexcept { return response == MessageResponse::Ok; }
};
InputBoxResult ShowInputBox(const InputBoxOptions& options = {});
struct Color {
std::uint8_t red = 0;
std::uint8_t green = 0;
std::uint8_t blue = 0;
friend bool operator==(Color left, Color right) noexcept {
return left.red == right.red && left.green == right.green && left.blue == right.blue;
}
friend bool operator!=(Color left, Color right) noexcept { return !(left == right); }
};
struct ColorPickerOptions {
std::string title;
Color initialColor{};
};
struct ColorPickerResult {
MessageResponse response = MessageResponse::Cancel;
Color color{};
std::string errorMessage;
explicit operator bool() const noexcept { return response == MessageResponse::Ok; }
};
ColorPickerResult PickColor(const ColorPickerOptions& options = {});
} // namespace izo

View File

@@ -1,5 +1,26 @@
#include <izo/Dialogs.hpp>
#include <izo/MessageBox.hpp>
// Platform implementations live in dialogs_windows.cpp and dialogs_linux.cpp.
namespace izo {
MessageResponse ShowMessageBox(std::string_view title, std::string_view message,
MessageIcon icon, MessageButtons buttons,
std::string* errorMessage) {
return ShowMessageBox({std::string(title), std::string(message), icon, buttons}, errorMessage);
}
MessageResponse ShowErrorBox(std::string_view title, std::string_view message,
std::string* errorMessage) {
return ShowMessageBox(title, message, MessageIcon::Error, MessageButtons::Ok, errorMessage);
}
MessageResponse ShowQuestionBox(std::string_view title, std::string_view message,
std::string* errorMessage) {
return ShowMessageBox(title, message, MessageIcon::Question, MessageButtons::YesNo, errorMessage);
}
} // namespace izo
// This translation unit ensures the archive always has a platform-independent
// object and is a natural home for future shared helpers.

View File

@@ -5,6 +5,7 @@
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sstream>
@@ -63,6 +64,11 @@ command_result run(const std::vector<std::string>& args, std::string_view input
return {WIFEXITED(status) ? WEXITSTATUS(status) : -1, std::move(output), {}};
}
std::string trimmed(std::string value) {
while (!value.empty() && (value.back() == '\n' || value.back() == '\r')) value.pop_back();
return value;
}
} // namespace
MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* errorMessage) {
@@ -109,6 +115,55 @@ MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* er
return MessageResponse::Error;
}
InputBoxResult ShowInputBox(const InputBoxOptions& options) {
std::vector<std::string> args;
if (on_path("zenity")) {
args = {"zenity", "--entry", "--title=" + options.title,
"--text=" + options.message, "--entry-text=" + options.initialValue};
if (options.password) args.push_back("--hide-text");
} else if (on_path("kdialog")) {
args = {"kdialog", options.password ? "--password" : "--inputbox", options.message};
if (!options.password) args.push_back(options.initialValue);
args.push_back("--title"); args.push_back(options.title);
} else {
return {MessageResponse::Error, {},
"No supported input box provider found; install zenity or kdialog"};
}
auto result = run(args);
if (result.code == 0) return {MessageResponse::Ok, trimmed(std::move(result.output)), {}};
if (result.code == 1) return {MessageResponse::Cancel, {}, {}};
return {MessageResponse::Error, {}, result.output.empty() ? "Input box failed" : trimmed(result.output)};
}
ColorPickerResult PickColor(const ColorPickerOptions& options) {
char initial[8];
std::snprintf(initial, sizeof(initial), "#%02x%02x%02x", options.initialColor.red,
options.initialColor.green, options.initialColor.blue);
std::vector<std::string> args;
if (on_path("zenity")) {
args = {"zenity", "--color-selection", "--show-palette", "--title=" + options.title,
std::string("--color=") + initial};
} else if (on_path("kdialog")) {
args = {"kdialog", "--getcolor", initial, "--title", options.title};
} else {
return {MessageResponse::Error, {},
"No supported color picker provider found; install zenity or kdialog"};
}
auto result = run(args);
if (result.code == 1) return {MessageResponse::Cancel, {}, {}};
if (result.code != 0)
return {MessageResponse::Error, {}, result.output.empty() ? "Color picker failed" : trimmed(result.output)};
const auto value = trimmed(std::move(result.output));
unsigned red = 0, green = 0, blue = 0;
if (std::sscanf(value.c_str(), "#%02x%02x%02x", &red, &green, &blue) != 3 &&
std::sscanf(value.c_str(), "rgb(%u,%u,%u)", &red, &green, &blue) != 3) {
return {MessageResponse::Error, {}, "Color picker returned an invalid color: " + value};
}
return {MessageResponse::Ok,
{static_cast<std::uint8_t>(red), static_cast<std::uint8_t>(green),
static_cast<std::uint8_t>(blue)}, {}};
}
bool SetClipboardText(std::string_view text, std::string* errorMessage) {
std::vector<std::string> args;
if (on_path("wl-copy")) args = {"wl-copy", "--type", "text/plain;charset=utf-8"};

View File

@@ -2,7 +2,9 @@
#include <izo/MessageBox.hpp>
#include <windows.h>
#include <commdlg.h>
#include <array>
#include <string>
namespace izo {
@@ -32,6 +34,79 @@ std::string windows_error(const char* operation) {
return std::string(operation) + " failed with error " + std::to_string(GetLastError());
}
constexpr int input_edit_id = 1001;
constexpr int input_ok_id = IDOK;
constexpr int input_cancel_id = IDCANCEL;
struct input_window_state {
InputBoxOptions options;
InputBoxResult result;
HWND edit = nullptr;
};
UINT_PTR CALLBACK color_picker_hook(HWND dialog, UINT message, WPARAM, LPARAM lparam) {
if (message == WM_INITDIALOG) {
const auto* picker = reinterpret_cast<const CHOOSECOLORW*>(lparam);
const auto* title = reinterpret_cast<const wchar_t*>(picker->lCustData);
if (title && *title) SetWindowTextW(dialog, title);
}
return 0;
}
LRESULT CALLBACK input_window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) {
auto* state = reinterpret_cast<input_window_state*>(GetWindowLongPtrW(window, GWLP_USERDATA));
if (message == WM_NCCREATE) {
state = static_cast<input_window_state*>(
reinterpret_cast<CREATESTRUCTW*>(lparam)->lpCreateParams);
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(state));
}
if (!state) return DefWindowProcW(window, message, wparam, lparam);
switch (message) {
case WM_CREATE: {
const auto prompt = widen(state->options.message);
const auto initial = widen(state->options.initialValue);
auto font = static_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
HWND label = CreateWindowW(L"STATIC", prompt.c_str(), WS_CHILD | WS_VISIBLE,
14, 14, 392, 36, window, nullptr, nullptr, nullptr);
DWORD edit_style = WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL;
if (state->options.password) edit_style |= ES_PASSWORD;
state->edit = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", initial.c_str(), edit_style,
14, 55, 392, 24, window,
reinterpret_cast<HMENU>(input_edit_id), nullptr, nullptr);
HWND ok = CreateWindowW(L"BUTTON", L"OK", WS_CHILD | WS_VISIBLE | WS_TABSTOP |
BS_DEFPUSHBUTTON, 245, 94, 76, 26, window,
reinterpret_cast<HMENU>(input_ok_id), nullptr, nullptr);
HWND cancel = CreateWindowW(L"BUTTON", L"Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
330, 94, 76, 26, window,
reinterpret_cast<HMENU>(input_cancel_id), nullptr, nullptr);
for (HWND control : {label, state->edit, ok, cancel})
SendMessageW(control, WM_SETFONT, reinterpret_cast<WPARAM>(font), TRUE);
SendMessageW(state->edit, EM_SETSEL, 0, -1);
SetFocus(state->edit);
return 0;
}
case WM_COMMAND:
if (LOWORD(wparam) == input_ok_id) {
const int length = GetWindowTextLengthW(state->edit);
std::wstring value(static_cast<std::size_t>(length) + 1, L'\0');
GetWindowTextW(state->edit, value.data(), length + 1);
value.resize(static_cast<std::size_t>(length));
state->result = {MessageResponse::Ok, narrow(value.c_str()), {}};
DestroyWindow(window);
return 0;
}
if (LOWORD(wparam) == input_cancel_id) {
DestroyWindow(window);
return 0;
}
break;
case WM_CLOSE:
DestroyWindow(window);
return 0;
}
return DefWindowProcW(window, message, wparam, lparam);
}
} // namespace
MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* errorMessage) {
@@ -61,6 +136,65 @@ MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* er
}
}
InputBoxResult ShowInputBox(const InputBoxOptions& options) {
static const wchar_t* class_name = L"iZoInputBox";
static const ATOM window_class = [] {
WNDCLASSW descriptor{};
descriptor.lpfnWndProc = input_window_proc;
descriptor.hInstance = GetModuleHandleW(nullptr);
descriptor.hCursor = LoadCursorW(nullptr, IDC_ARROW);
descriptor.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
descriptor.lpszClassName = class_name;
return RegisterClassW(&descriptor);
}();
if (!window_class && GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
return {MessageResponse::Error, {}, windows_error("RegisterClassW")};
input_window_state state{options};
const auto title = widen(options.title);
HWND window = CreateWindowExW(WS_EX_DLGMODALFRAME | WS_EX_CONTROLPARENT, class_name,
title.c_str(), WS_CAPTION | WS_SYSMENU,
CW_USEDEFAULT, CW_USEDEFAULT, 436, 164, nullptr, nullptr,
GetModuleHandleW(nullptr), &state);
if (!window) return {MessageResponse::Error, {}, windows_error("CreateWindowExW")};
RECT bounds{};
GetWindowRect(window, &bounds);
const int x = (GetSystemMetrics(SM_CXSCREEN) - (bounds.right - bounds.left)) / 2;
const int y = (GetSystemMetrics(SM_CYSCREEN) - (bounds.bottom - bounds.top)) / 2;
SetWindowPos(window, HWND_TOP, x, y, 0, 0, SWP_NOSIZE);
ShowWindow(window, SW_SHOW);
MSG message{};
while (IsWindow(window) && GetMessageW(&message, nullptr, 0, 0) > 0) {
if (!IsDialogMessageW(window, &message)) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
}
return state.result;
}
ColorPickerResult PickColor(const ColorPickerOptions& options) {
std::array<COLORREF, 16> custom_colors{};
const auto title = widen(options.title);
CHOOSECOLORW picker{};
picker.lStructSize = sizeof(picker);
picker.rgbResult = RGB(options.initialColor.red, options.initialColor.green,
options.initialColor.blue);
picker.lpCustColors = custom_colors.data();
picker.lCustData = reinterpret_cast<LPARAM>(title.c_str());
picker.lpfnHook = color_picker_hook;
picker.Flags = CC_ENABLEHOOK | CC_FULLOPEN | CC_RGBINIT;
if (ChooseColorW(&picker)) {
return {MessageResponse::Ok,
{GetRValue(picker.rgbResult), GetGValue(picker.rgbResult),
GetBValue(picker.rgbResult)}, {}};
}
const DWORD error = CommDlgExtendedError();
if (error == 0) return {MessageResponse::Cancel, options.initialColor, {}};
return {MessageResponse::Error, options.initialColor,
"ChooseColorW failed with error " + std::to_string(error)};
}
bool SetClipboardText(std::string_view text, std::string* errorMessage) {
const auto value = widen(text);
if (!OpenClipboard(nullptr)) {