// Copyright (c) 2005, Niels Martin Hansen
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of the Aegisub Group nor the names of its contributors
//     may be used to endorse or promote products derived from this software
//     without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Aegisub Project http://www.aegisub.org/

#include "colorspace.h"
#include "compat.h"
#include "help_button.h"
#include "libresrc/libresrc.h"
#include "options.h"
#include "persist_location.h"
#include "utils.h"
#include "value_event.h"
#include "xdg_desktop_portal_utils.h"

#include <libaegisub/log.h>
#include <libaegisub/scoped_ptr.h>

#include <algorithm>
#include <cmath>
#include <memory>
#include <vector>

#include <wx/bitmap.h>
#include <wx/bmpbuttn.h>
#include <wx/button.h>
#include <wx/choice.h>
#include <wx/dcbuffer.h>
#include <wx/dcclient.h>
#include <wx/dcmemory.h>
#include <wx/dcscreen.h>
#include <wx/dialog.h>
#include <wx/event.h>
#include <wx/image.h>
#include <wx/rawbmp.h>
#include <wx/settings.h>
#include <wx/sizer.h>
#include <wx/spinctrl.h>
#include <wx/statbmp.h>
#include <wx/statbox.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>

#ifdef __WXMAC__
namespace osx { void DropFromScreen(int x, int y, int resx, int resy, int magnification, wxMemoryDC &capdc); }
#endif

namespace {

enum class PickerDirection {
	HorzVert,
	Horz,
	Vert
};

static const int spectrum_horz_vert_arrow_size = 4;

#ifdef WITH_LIBPORTAL
// this could be made configurable
static constexpr bool enable_os_eyedropper = true;
static constexpr bool enable_screenshot_eyedropper = false;
#else
static constexpr bool enable_os_eyedropper = false;
static constexpr bool enable_screenshot_eyedropper = true;
#endif

wxDEFINE_EVENT(EVT_SPECTRUM_CHANGE, wxCommandEvent);

class ColorPickerSpectrum final : public wxControl {
	int x;
	int y;

	wxBitmap *background;
	PickerDirection direction;

	void OnPaint(wxPaintEvent &) {
		if (!background) return;

		int height = background->GetHeight();
		int width = background->GetWidth();
		wxPaintDC dc(this);

		wxMemoryDC memdc;
		memdc.SelectObject(*background);
		dc.Blit(1, 1, width, height, &memdc, 0, 0);

		wxPoint arrow[3];
		wxRect arrow_box;

		wxPen invpen(*wxWHITE, 3);
		invpen.SetCap(wxCAP_BUTT);
		dc.SetLogicalFunction(wxXOR);
		dc.SetPen(invpen);

		switch (direction) {
			case PickerDirection::HorzVert:
				// Make a little cross
				dc.DrawLine(x-4, y+1, x+7, y+1);
				dc.DrawLine(x+1, y-4, x+1, y+7);
				break;
			case PickerDirection::Horz:
				// Make a vertical line stretching all the way across
				dc.DrawLine(x+1, 1, x+1, height+1);
				// Points for arrow
				arrow[0] = wxPoint(x+1, height+2);
				arrow[1] = wxPoint(x+1-spectrum_horz_vert_arrow_size, height+2+spectrum_horz_vert_arrow_size);
				arrow[2] = wxPoint(x+1+spectrum_horz_vert_arrow_size, height+2+spectrum_horz_vert_arrow_size);

				arrow_box.SetLeft(0);
				arrow_box.SetTop(height + 2);
				arrow_box.SetRight(width + 1 + spectrum_horz_vert_arrow_size);
				arrow_box.SetBottom(height + 2 + spectrum_horz_vert_arrow_size);
				break;
			case PickerDirection::Vert:
				// Make a horizontal line stretching all the way across
				dc.DrawLine(1, y+1, width+1, y+1);
				// Points for arrow
				arrow[0] = wxPoint(width+2, y+1);
				arrow[1] = wxPoint(width+2+spectrum_horz_vert_arrow_size, y+1-spectrum_horz_vert_arrow_size);
				arrow[2] = wxPoint(width+2+spectrum_horz_vert_arrow_size, y+1+spectrum_horz_vert_arrow_size);

				arrow_box.SetLeft(width + 2);
				arrow_box.SetTop(0);
				arrow_box.SetRight(width + 2 + spectrum_horz_vert_arrow_size);
				arrow_box.SetBottom(height + 1 + spectrum_horz_vert_arrow_size);
				break;
		}

		if (direction == PickerDirection::Horz || direction == PickerDirection::Vert) {
			wxBrush bgBrush;
			bgBrush.SetColour(GetBackgroundColour());
			dc.SetLogicalFunction(wxCOPY);
			dc.SetPen(*wxTRANSPARENT_PEN);
			dc.SetBrush(bgBrush);
			dc.DrawRectangle(arrow_box);

			// Arrow pointing at current point
			dc.SetBrush(*wxBLACK_BRUSH);
			dc.DrawPolygon(3, arrow);
		}

		// Border around the spectrum
		wxPen blkpen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), 1);
		blkpen.SetCap(wxCAP_BUTT);

		dc.SetLogicalFunction(wxCOPY);
		dc.SetPen(blkpen);
		dc.SetBrush(*wxTRANSPARENT_BRUSH);
		dc.DrawRectangle(0, 0, background->GetWidth()+2, background->GetHeight()+2);
	}

	void OnMouse(wxMouseEvent &evt) {
		evt.Skip();

		// We only care about mouse move events during a drag
		if (evt.Moving())
			return;

		if (evt.LeftDown()) {
			CaptureMouse();
			SetCursor(wxCursor(wxCURSOR_BLANK));
		}
		else if (evt.LeftUp() && HasCapture()) {
			ReleaseMouse();
			SetCursor(wxNullCursor);
		}

		if (evt.LeftDown() || (HasCapture() && evt.LeftIsDown())) {
			// Adjust for the 1px black border around the control
			int newx = mid(0, evt.GetX() - 1, GetClientSize().x - 3);
			int newy = mid(0, evt.GetY() - 1, GetClientSize().y - 3);
			SetXY(newx, newy);
			wxCommandEvent evt2(EVT_SPECTRUM_CHANGE, GetId());
			AddPendingEvent(evt2);
		}
	}

	bool AcceptsFocusFromKeyboard() const override { return false; }

public:
	ColorPickerSpectrum(wxWindow *parent, PickerDirection direction, wxSize size)
	: wxControl(parent, -1, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
	, x(-1)
	, y(-1)
	, background(nullptr)
	, direction(direction)
	{
		size.x += 2;
		size.y += 2;

		if (direction == PickerDirection::Vert) size.x += spectrum_horz_vert_arrow_size + 1;
		if (direction == PickerDirection::Horz) size.y += spectrum_horz_vert_arrow_size + 1;

		SetClientSize(size);
		SetMinSize(GetSize());

		Bind(wxEVT_LEFT_DOWN, &ColorPickerSpectrum::OnMouse, this);
		Bind(wxEVT_LEFT_UP, &ColorPickerSpectrum::OnMouse, this);
		Bind(wxEVT_MOTION, &ColorPickerSpectrum::OnMouse, this);
		Bind(wxEVT_PAINT, &ColorPickerSpectrum::OnPaint, this);
	}

	int GetX() const { return x; }
	int GetY() const { return y; }

	void SetXY(int xx, int yy) {
		if (x != xx || y != yy) {
			x = xx;
			y = yy;
			Refresh(false);
		}
	}

	/// @brief Set the background image for this spectrum
	/// @param new_background New background image
	/// @param force Repaint even if it appears to be the same image
	void SetBackground(wxBitmap *new_background, bool force = false) {
		if (background == new_background && !force) return;
		background = new_background;
		Refresh(false);
	}
};

#ifdef WIN32
#define STATIC_BORDER_FLAG wxSTATIC_BORDER
#else
#define STATIC_BORDER_FLAG wxSIMPLE_BORDER
#endif

wxDEFINE_EVENT(EVT_RECENT_SELECT, ValueEvent<agi::Color>);

/// @class ColorPickerRecent
/// @brief A grid of recently used colors which can be selected by clicking on them
class ColorPickerRecent final : public wxControl {
	int rows;     ///< Number of rows of colors
	int cols;     ///< Number of cols of colors
	int cellsize; ///< Width/Height of each cell

	/// The colors currently displayed in the control
	std::vector<agi::Color> colors;

	void OnClick(wxMouseEvent &evt) {
		int cx = evt.GetX() / cellsize;
		int cy = evt.GetY() / cellsize;
		if (cx < 0 || cx > cols || cy < 0 || cy > rows) return;
		int i = cols*cy + cx;

		if (i >= 0 && i < (int)colors.size())
			AddPendingEvent(ValueEvent<agi::Color>(EVT_RECENT_SELECT, GetId(), colors[i]));
	}

	void OnPaint(wxPaintEvent &) {
		wxAutoBufferedPaintDC dc(this);
		dc.Clear();

		dc.SetPen(*wxTRANSPARENT_PEN);

		for (int cy = 0; cy < rows; cy++) {
			for (int cx = 0; cx < cols; cx++) {
				int x = cx * cellsize;
				int y = cy * cellsize;

				dc.SetBrush(wxBrush(to_wx(colors[cy * cols + cx])));
				dc.DrawRectangle(x, y, x+cellsize, y+cellsize);
			}
		}
	}

	bool AcceptsFocusFromKeyboard() const override { return false; }

public:
	ColorPickerRecent(wxWindow *parent, int cols, int rows, int cellsize)
	: wxControl(parent, -1, wxDefaultPosition, wxDefaultSize, STATIC_BORDER_FLAG)
	, rows(rows)
	, cols(cols)
	, cellsize(cellsize)
	{
		colors.resize(rows * cols);
		SetClientSize(cols*cellsize, rows*cellsize);
		SetMinSize(GetSize());
		SetMaxSize(GetSize());
		SetCursor(*wxCROSS_CURSOR);
		SetBackgroundStyle(wxBG_STYLE_PAINT);

		Bind(wxEVT_LEFT_DOWN, &ColorPickerRecent::OnClick, this);
		Bind(wxEVT_PAINT, &ColorPickerRecent::OnPaint, this);
	}

	/// Load the colors to show
	void Load(std::vector<agi::Color> const& recent_colors) {
		colors = recent_colors;
		colors.resize(rows * cols);
		Refresh(false);
	}

	/// Get the list of recent colors
	std::vector<agi::Color> Save() const { return colors; }

	/// Add a color to the beginning of the recent list
	void AddColor(agi::Color color) {
		auto existing = find(colors.begin(), colors.end(), color);
		if (existing != colors.end())
			rotate(colors.begin(), existing, existing + 1);
		else {
			colors.insert(colors.begin(), color);
			colors.pop_back();
		}

		Refresh(false);
	}
};

wxDEFINE_EVENT(EVT_DROPPER_SELECT, ValueEvent<agi::Color>);

class ColorPickerScreenDropper final : public wxControl {
	wxBitmap capture;

	int resx, resy;
	int magnification;

	void OnMouse(wxMouseEvent &evt) {
		int x = evt.GetX();
		int y = evt.GetY();

		if (x >= 0 && x < capture.GetWidth() && y >= 0 && y < capture.GetHeight()) {
			wxNativePixelData pd(capture, wxRect(x, y, 1, 1));
			wxNativePixelData::Iterator pdi(pd.GetPixels());
			agi::Color color(pdi.Red(), pdi.Green(), pdi.Blue(), 0);

			wxThreadEvent evnt(EVT_DROPPER_SELECT, GetId());
			AddPendingEvent(ValueEvent<agi::Color>(EVT_DROPPER_SELECT, GetId(), color));
		}
	}

	void OnPaint(wxPaintEvent &) {
		wxPaintDC(this).DrawBitmap(capture, 0, 0);
	}

	bool AcceptsFocusFromKeyboard() const override { return false; }

public:
	ColorPickerScreenDropper(wxWindow *parent, int resx, int resy, int magnification)
	: wxControl(parent, -1, wxDefaultPosition, wxDefaultSize, STATIC_BORDER_FLAG)
	, capture(resx * magnification, resy * magnification, wxNativePixelFormat::BitsPerPixel)
	, resx(resx)
	, resy(resy)
	, magnification(magnification)
	{
		SetClientSize(resx*magnification, resy*magnification);
		SetMinSize(GetSize());
		SetMaxSize(GetSize());
		SetCursor(*wxCROSS_CURSOR);

		wxMemoryDC capdc(capture);
		capdc.SetPen(*wxTRANSPARENT_PEN);
		capdc.SetBrush(*wxWHITE_BRUSH);
		capdc.DrawRectangle(0, 0, capture.GetWidth(), capture.GetHeight());

		Bind(wxEVT_PAINT, &ColorPickerScreenDropper::OnPaint, this);
		Bind(wxEVT_LEFT_DOWN, &ColorPickerScreenDropper::OnMouse, this);
	}

	void DropFromScreenXY(int x, int y);
};

void ColorPickerScreenDropper::DropFromScreenXY(int x, int y) {
	wxMemoryDC capdc(capture);
	capdc.SetPen(*wxTRANSPARENT_PEN);

#ifndef __WXMAC__
	wxScreenDC screen;
	capdc.StretchBlit(0, 0, resx * magnification, resy * magnification,
		&screen, x - resx / 2, y - resy / 2, resx, resy);
#else
	// wxScreenDC doesn't work on OS X so do it manually
	osx::DropFromScreen(x, y, resx, resy, magnification, capdc);
#endif

	Refresh(false);
}

wxDEFINE_EVENT(EVT_OS_SELECT, ValueEvent<agi::Color>);

class DialogColorPicker final : public wxDialog {
	std::unique_ptr<PersistLocation> persist;

	agi::Color cur_color; ///< Currently selected colour

	bool spectrum_dirty; ///< Does the spectrum image need to be regenerated?
	ColorPickerSpectrum *spectrum; ///< The 2D color spectrum
	ColorPickerSpectrum *slider; ///< The 1D slider for the color component not in the slider
	ColorPickerSpectrum *alpha_slider;

	wxChoice *colorspace_choice; ///< The dropdown list to select colorspaces

	wxSpinCtrl *rgb_input[3];
	wxBitmap rgb_spectrum[3]; ///< x/y spectrum bitmap where color "i" is excluded from
	wxBitmap rgb_slider[3];   ///< z spectrum for color "i"

	wxSpinCtrl *hsl_input[3];
	wxBitmap hsl_spectrum; ///< h/s spectrum
	wxBitmap hsl_slider;   ///< l spectrum

	wxSpinCtrl *hsv_input[3];
	wxBitmap hsv_spectrum; ///< s/v spectrum
	wxBitmap hsv_slider;   ///< h spectrum
	wxBitmap alpha_slider_img;

	wxTextCtrl *ass_input;
	wxTextCtrl *html_input;
	wxSpinCtrl *alpha_input;

	/// The eyedropper is set to a blank icon when it's clicked, so store its normal bitmap
	wxBitmapBundle eyedropper_bitmap;

	/// The point where the eyedropper was click, used to make it possible to either
	/// click the eyedropper or drag the eyedropper
	wxPoint eyedropper_grab_point;

	bool eyedropper_is_grabbed;

	wxStaticBitmap *preview_box; ///< A box which simply shows the current color
	ColorPickerRecent *recent_box; ///< A grid of recently used colors

	ColorPickerScreenDropper *screenshot_screen_dropper = nullptr;
	wxStaticBitmap *screenshot_screen_dropper_icon = nullptr;

	wxBitmapButton *os_screen_dropper_button = nullptr;

	/// Update all other controls as a result of modifying an RGB control
	void UpdateFromRGB(bool dirty = true);
	/// Update all other controls as a result of modifying an HSL control
	void UpdateFromHSL(bool dirty = true);
	/// Update all other controls as a result of modifying an HSV control
	void UpdateFromHSV(bool dirty = true);
	/// Update all other controls as a result of modifying the ASS format control
	void UpdateFromAss();
	/// Update all other controls as a result of modifying the HTML format control
	void UpdateFromHTML();
	void UpdateFromAlpha();

	void SetRGB(agi::Color new_color);
	void SetHSL(unsigned char r, unsigned char g, unsigned char b);
	void SetHSV(unsigned char r, unsigned char g, unsigned char b);

	/// Redraw the spectrum display
	void UpdateSpectrumDisplay();

	wxBitmap *MakeGBSpectrum();
	wxBitmap *MakeRBSpectrum();
	wxBitmap *MakeRGSpectrum();
	wxBitmap *MakeHSSpectrum();
	wxBitmap *MakeSVSpectrum();

	/// Constructor helper function for making the color input box sizers
	template<int N, class Control>
	wxSizer *MakeColorInputSizer(wxWindow *parent, wxString (&labels)[N], Control *(&inputs)[N]);

	void OnChangeMode(wxCommandEvent &evt);
	void OnSpectrumChange(wxCommandEvent &evt);
	void OnSliderChange(wxCommandEvent &evt);
	void OnAlphaSliderChange(wxCommandEvent &evt);
	void OnRecentSelect(ValueEvent<agi::Color>& evt); // also handles dropper pick
	void OnDropperMouse(wxMouseEvent &evt);
	void OnMouse(wxMouseEvent &evt);
	void OnCaptureLost(wxMouseCaptureLostEvent&);
	void OnOsDropperClick(wxCommandEvent&);

	std::function<void (agi::Color)> callback;

public:
	DialogColorPicker(wxWindow *parent, agi::Color initial_color, std::function<void (agi::Color)> callback, bool alpha);
	~DialogColorPicker();

	void SetColor(agi::Color new_color);
	void AddColorToRecent();
};

static const int slider_width = 10; ///< width in pixels of the color slider control
static const int alpha_box_size = 5;

template<typename Func>
static wxBitmap make_slider_img(Func func) {
	unsigned char *slid = (unsigned char *)calloc(slider_width * 256 * 3, 1);
	func(slid);
	wxImage img(slider_width, 256, slid);
	return wxBitmap(img);
}

template<typename Func>
static wxBitmap make_slider(Func func) {
	return make_slider_img([&](unsigned char *slid) {
		for (int y = 0; y < 256; ++y) {
			unsigned char rgb[3];
			func(y, rgb);
			for (int x = 0; x < slider_width; ++x)
				memcpy(slid + y * slider_width * 3 + x * 3, rgb, 3);
		}
	});
}

DialogColorPicker::DialogColorPicker(wxWindow *parent, agi::Color initial_color, std::function<void (agi::Color)> callback, bool alpha)
: wxDialog(parent, -1, _("Select Color"))
, callback(std::move(callback))
{
	// generate spectrum slider bar images
	for (int i = 0; i < 3; ++i) {
		rgb_slider[i] = make_slider([=](int y, unsigned char *rgb) {
			memset(rgb, 0, 3);
			rgb[i] = y;
		});
	}
	hsl_slider = make_slider([](int y, unsigned char *rgb) { memset(rgb, y, 3); });
	hsv_slider = make_slider([](int y, unsigned char *rgb) { hsv_to_rgb(y, 255, 255, rgb, rgb + 1, rgb + 2); });

	// Create the controls for the dialog
	wxStaticBoxSizer *spectrum_box_sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Color spectrum"));
	wxWindow *spectrum_box = spectrum_box_sizer->GetStaticBox();

	spectrum = new ColorPickerSpectrum(spectrum_box, PickerDirection::HorzVert, wxSize(256, 256));
	slider = new ColorPickerSpectrum(spectrum_box, PickerDirection::Vert, wxSize(slider_width, 256));
	alpha_slider = new ColorPickerSpectrum(spectrum_box, PickerDirection::Vert, wxSize(slider_width, 256));
	wxString modes[] = { _("RGB/R"), _("RGB/G"), _("RGB/B"), _("HSL/L"), _("HSV/H") };
	colorspace_choice = new wxChoice(spectrum_box, -1, wxDefaultPosition, wxDefaultSize, 5, modes);

	wxStaticBoxSizer *rgb_box_sizer = new wxStaticBoxSizer(wxHORIZONTAL, this, _("RGB color"));
	wxStaticBoxSizer *hsl_box_sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("HSL color"));
	wxStaticBoxSizer *hsv_box_sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("HSV color"));

	wxWindow *rgb_box = rgb_box_sizer->GetStaticBox();
	wxWindow *hsl_box = hsl_box_sizer->GetStaticBox();
	wxWindow *hsv_box = hsv_box_sizer->GetStaticBox();

	for (auto& elem : rgb_input)
		elem = new wxSpinCtrl(rgb_box, -1, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 255);

	auto dummy = new wxTextCtrl(this, -1);
	wxSize colorinput_size = dummy->GetSizeFromText("&H000000&");
	dummy->Destroy();

	ass_input = new wxTextCtrl(rgb_box, -1, "", wxDefaultPosition, colorinput_size);
	html_input = new wxTextCtrl(rgb_box, -1, "", wxDefaultPosition, colorinput_size);
	alpha_input = new wxSpinCtrl(rgb_box, -1, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 255);

	for (auto& elem : hsl_input)
		elem = new wxSpinCtrl(hsl_box, -1, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 255);

	for (auto& elem : hsv_input)
		elem = new wxSpinCtrl(hsv_box, -1, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 255);

	preview_box = new wxStaticBitmap(spectrum_box, -1, wxBitmap(40, 40, 24), wxDefaultPosition, wxSize(40, 40), STATIC_BORDER_FLAG);
	recent_box = new ColorPickerRecent(this, 8, 4, 16);

	eyedropper_bitmap = GETBUNDLE(eyedropper_tool, 24);
	if (enable_os_eyedropper) {
		os_screen_dropper_button = new wxBitmapButton(this, wxID_ANY, eyedropper_bitmap, wxDefaultPosition, wxDefaultSize, wxBORDER_DEFAULT);
	}
	if (enable_screenshot_eyedropper) {
		screenshot_screen_dropper_icon = new wxStaticBitmap(this, -1, eyedropper_bitmap, wxDefaultPosition, wxDefaultSize, wxRAISED_BORDER);
		screenshot_screen_dropper = new ColorPickerScreenDropper(this, 7, 7, 8);
	}

	// Arrange the controls in a nice way
	wxSizer *spectop_sizer = new wxBoxSizer(wxHORIZONTAL);
	spectop_sizer->Add(new wxStaticText(spectrum_box, -1, _("Spectrum mode:")), 0, wxALIGN_CENTER_VERTICAL|wxALIGN_LEFT|wxRIGHT, 5);
	spectop_sizer->Add(colorspace_choice, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_LEFT);
	spectop_sizer->Add(5, 5, 1, wxEXPAND);
	spectop_sizer->Add(preview_box, 0, wxALIGN_CENTER_VERTICAL);

	wxSizer *spectrum_sizer = new wxFlexGridSizer(3, 5, 5);
	spectrum_sizer->Add(spectop_sizer, wxEXPAND);
	spectrum_sizer->AddStretchSpacer(1);
	spectrum_sizer->AddStretchSpacer(1);
	spectrum_sizer->Add(spectrum);
	spectrum_sizer->Add(slider);
	spectrum_sizer->Add(alpha_slider);
	if (!alpha)
		spectrum_sizer->Hide(alpha_slider);

	spectrum_box_sizer->Add(spectrum_sizer, 0, wxALL, 3);

	wxString rgb_labels[] = { _("Red:"), _("Green:"), _("Blue:") };
	rgb_box_sizer->Add(MakeColorInputSizer(rgb_box, rgb_labels, rgb_input), 1, wxALL|wxEXPAND, 3);

	wxString ass_labels[] = { "ASS:", "HTML:", _("Alpha:") };
	wxControl *ass_ctrls[] = { ass_input, html_input, alpha_input };
	auto ass_colors_sizer = MakeColorInputSizer(rgb_box, ass_labels, ass_ctrls);
	if (!alpha)
		ass_colors_sizer->Hide(alpha_input);
	rgb_box_sizer->Add(ass_colors_sizer, 0, wxALL|wxEXPAND, 3);

	wxString hsl_labels[] = { _("Hue:"), _("Sat.:"), _("Lum.:") };
	hsl_box_sizer->Add(MakeColorInputSizer(hsl_box, hsl_labels, hsl_input), 0, wxALL|wxEXPAND, 3);

	wxString hsv_labels[] = { _("Hue:"), _("Sat.:"), _("Value:") };
	hsv_box_sizer->Add(MakeColorInputSizer(hsv_box, hsv_labels, hsv_input), 0, wxALL|wxEXPAND, 3);

	wxSizer *hsx_sizer = new wxBoxSizer(wxHORIZONTAL);
	hsx_sizer->Add(hsl_box_sizer);
	hsx_sizer->AddSpacer(5);
	hsx_sizer->Add(hsv_box_sizer);

	wxSizer *picker_sizer = new wxBoxSizer(wxHORIZONTAL);
	picker_sizer->AddStretchSpacer();
	if (os_screen_dropper_button) {
		picker_sizer->Add(os_screen_dropper_button, 0, wxALIGN_CENTER);
		picker_sizer->AddStretchSpacer();
	}
	if (screenshot_screen_dropper) {
		picker_sizer->Add(screenshot_screen_dropper_icon, 0, wxALIGN_CENTER|wxRIGHT, 5);
		picker_sizer->Add(screenshot_screen_dropper, 0, wxALIGN_CENTER);
		picker_sizer->AddStretchSpacer();
	}
	picker_sizer->Add(recent_box, 0, wxALIGN_CENTER);
	picker_sizer->AddStretchSpacer();

	wxStdDialogButtonSizer *button_sizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL | wxHELP);

	wxSizer *input_sizer = new wxBoxSizer(wxVERTICAL);
	input_sizer->Add(rgb_box_sizer, 0, wxEXPAND);
	input_sizer->AddSpacer(5);
	input_sizer->Add(hsx_sizer, 0, wxEXPAND);
	input_sizer->AddStretchSpacer(1);
	input_sizer->Add(picker_sizer, 0, wxEXPAND);
	input_sizer->AddStretchSpacer(2);
	input_sizer->Add(button_sizer, 0, wxALIGN_RIGHT);

	wxSizer *main_sizer = new wxBoxSizer(wxHORIZONTAL);
	main_sizer->Add(spectrum_box_sizer, 1, wxALL | wxEXPAND, 5);
	main_sizer->Add(input_sizer, 0, (wxALL&~wxLEFT)|wxEXPAND, 5);

	SetSizerAndFit(main_sizer);

	persist = std::make_unique<PersistLocation>(this, "Tool/Colour Picker");

	// Fill the controls
	int mode = OPT_GET("Tool/Colour Picker/Mode")->GetInt();
	if (mode < 0 || mode > 4) mode = 3; // HSL default
	colorspace_choice->SetSelection(mode);
	SetColor(initial_color);
	recent_box->Load(OPT_GET("Tool/Colour Picker/Recent Colours")->GetListColor());

	using std::bind;
	for (int i = 0; i < 3; ++i) {
		rgb_input[i]->Bind(wxEVT_SPINCTRL, bind(&DialogColorPicker::UpdateFromRGB, this, true));
		rgb_input[i]->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromRGB, this, true));
		hsl_input[i]->Bind(wxEVT_SPINCTRL, bind(&DialogColorPicker::UpdateFromHSL, this, true));
		hsl_input[i]->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromHSL, this, true));
		hsv_input[i]->Bind(wxEVT_SPINCTRL, bind(&DialogColorPicker::UpdateFromHSV, this, true));
		hsv_input[i]->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromHSV, this, true));
	}
	ass_input->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromAss, this));
	html_input->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromHTML, this));
	alpha_input->Bind(wxEVT_SPINCTRL, bind(&DialogColorPicker::UpdateFromAlpha, this));
	alpha_input->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromAlpha, this));

	if (screenshot_screen_dropper) {
		screenshot_screen_dropper_icon->Bind(wxEVT_MOTION, &DialogColorPicker::OnDropperMouse, this);
		screenshot_screen_dropper_icon->Bind(wxEVT_LEFT_DOWN, &DialogColorPicker::OnDropperMouse, this);
		screenshot_screen_dropper_icon->Bind(wxEVT_LEFT_UP, &DialogColorPicker::OnDropperMouse, this);
		screenshot_screen_dropper_icon->Bind(wxEVT_MOUSE_CAPTURE_LOST, &DialogColorPicker::OnCaptureLost, this);
		Bind(wxEVT_MOTION, &DialogColorPicker::OnMouse, this);
		Bind(wxEVT_LEFT_DOWN, &DialogColorPicker::OnMouse, this);
		Bind(wxEVT_LEFT_UP, &DialogColorPicker::OnMouse, this);
	}

	if (os_screen_dropper_button)
		os_screen_dropper_button->Bind(wxEVT_BUTTON, &DialogColorPicker::OnOsDropperClick, this);

	spectrum->Bind(EVT_SPECTRUM_CHANGE, &DialogColorPicker::OnSpectrumChange, this);
	slider->Bind(EVT_SPECTRUM_CHANGE, &DialogColorPicker::OnSliderChange, this);
	alpha_slider->Bind(EVT_SPECTRUM_CHANGE, &DialogColorPicker::OnAlphaSliderChange, this);
	recent_box->Bind(EVT_RECENT_SELECT, &DialogColorPicker::OnRecentSelect, this);
	if (screenshot_screen_dropper)
		screenshot_screen_dropper->Bind(EVT_DROPPER_SELECT, &DialogColorPicker::OnRecentSelect, this);
	Bind(EVT_OS_SELECT, &DialogColorPicker::OnRecentSelect, this);

	colorspace_choice->Bind(wxEVT_CHOICE, &DialogColorPicker::OnChangeMode, this);

	button_sizer->GetHelpButton()->Bind(wxEVT_BUTTON, bind(&HelpButton::OpenPage, "Colour Picker"));
}

template<int N, class Control>
wxSizer *DialogColorPicker::MakeColorInputSizer(wxWindow *parent, wxString (&labels)[N], Control *(&inputs)[N]) {
	auto sizer = new wxFlexGridSizer(2, 5, 5);
	for (int i = 0; i < N; ++i) {
		sizer->Add(new wxStaticText(parent, -1, labels[i]), wxSizerFlags(1).Center().Left());
		sizer->Add(inputs[i], wxSizerFlags().Expand());
	}
	sizer->AddGrowableCol(0,1);
	return sizer;
}

DialogColorPicker::~DialogColorPicker() {
	if (screenshot_screen_dropper && screenshot_screen_dropper_icon->HasCapture()) screenshot_screen_dropper_icon->ReleaseMouse();
}

static void change_value(wxSpinCtrl *ctrl, int value) {
	wxEventBlocker blocker(ctrl);
	ctrl->SetValue(value);
}

void DialogColorPicker::SetColor(agi::Color new_color) {
	change_value(alpha_input, new_color.a);
	alpha_slider->SetXY(0, new_color.a);
	cur_color.a = new_color.a;

	SetRGB(new_color);
	spectrum_dirty = true;
	UpdateFromRGB();
}

void DialogColorPicker::AddColorToRecent() {
	recent_box->AddColor(cur_color);
	OPT_SET("Tool/Colour Picker/Recent Colours")->SetListColor(recent_box->Save());
}

void DialogColorPicker::SetRGB(agi::Color new_color) {
	change_value(rgb_input[0], new_color.r);
	change_value(rgb_input[1], new_color.g);
	change_value(rgb_input[2], new_color.b);
	new_color.a = cur_color.a;
	cur_color = new_color;
}

void DialogColorPicker::SetHSL(unsigned char r, unsigned char g, unsigned char b) {
	unsigned char h, s, l;
	rgb_to_hsl(r, g, b, &h, &s, &l);
	change_value(hsl_input[0], h);
	change_value(hsl_input[1], s);
	change_value(hsl_input[2], l);
}

void DialogColorPicker::SetHSV(unsigned char r, unsigned char g, unsigned char b) {
	unsigned char h, s, v;
	rgb_to_hsv(r, g, b, &h, &s, &v);
	change_value(hsv_input[0], h);
	change_value(hsv_input[1], s);
	change_value(hsv_input[2], v);
}

void DialogColorPicker::UpdateFromRGB(bool dirty) {
	unsigned char r = rgb_input[0]->GetValue();
	unsigned char g = rgb_input[1]->GetValue();
	unsigned char b = rgb_input[2]->GetValue();
	SetHSL(r, g, b);
	SetHSV(r, g, b);
	cur_color = agi::Color(r, g, b, cur_color.a);
	ass_input->ChangeValue(to_wx(cur_color.GetAssOverrideFormatted()));
	html_input->ChangeValue(to_wx(cur_color.GetHexFormatted()));

	if (dirty)
		spectrum_dirty = true;
	UpdateSpectrumDisplay();
}

void DialogColorPicker::UpdateFromHSL(bool dirty) {
	unsigned char r, g, b, h, s, l;
	h = hsl_input[0]->GetValue();
	s = hsl_input[1]->GetValue();
	l = hsl_input[2]->GetValue();
	hsl_to_rgb(h, s, l, &r, &g, &b);
	SetRGB(agi::Color(r, g, b));
	SetHSV(r, g, b);

	ass_input->ChangeValue(to_wx(cur_color.GetAssOverrideFormatted()));
	html_input->ChangeValue(to_wx(cur_color.GetHexFormatted()));

	if (dirty)
		spectrum_dirty = true;
	UpdateSpectrumDisplay();
}

void DialogColorPicker::UpdateFromHSV(bool dirty) {
	unsigned char r, g, b, h, s, v;
	h = hsv_input[0]->GetValue();
	s = hsv_input[1]->GetValue();
	v = hsv_input[2]->GetValue();
	hsv_to_rgb(h, s, v, &r, &g, &b);
	SetRGB(agi::Color(r, g, b));
	SetHSL(r, g, b);
	ass_input->ChangeValue(to_wx(cur_color.GetAssOverrideFormatted()));
	html_input->ChangeValue(to_wx(cur_color.GetHexFormatted()));

	if (dirty)
		spectrum_dirty = true;
	UpdateSpectrumDisplay();
}

void DialogColorPicker::UpdateFromAss() {
	agi::Color color(from_wx(ass_input->GetValue()));
	SetRGB(color);
	SetHSL(color.r, color.g, color.b);
	SetHSV(color.r, color.g, color.b);
	html_input->ChangeValue(to_wx(cur_color.GetHexFormatted()));

	spectrum_dirty = true;
	UpdateSpectrumDisplay();
}

void DialogColorPicker::UpdateFromHTML() {
	agi::Color color(from_wx(html_input->GetValue()));
	SetRGB(color);
	SetHSL(color.r, color.g, color.b);
	SetHSV(color.r, color.g, color.b);
	ass_input->ChangeValue(to_wx(cur_color.GetAssOverrideFormatted()));

	spectrum_dirty = true;
	UpdateSpectrumDisplay();
}

void DialogColorPicker::UpdateFromAlpha() {
	cur_color.a = alpha_input->GetValue();
	alpha_slider->SetXY(0, cur_color.a);
	callback(cur_color);
}

void DialogColorPicker::UpdateSpectrumDisplay() {
	int i = colorspace_choice->GetSelection();
	if (spectrum_dirty) {
		switch (i) {
			case 0: spectrum->SetBackground(MakeGBSpectrum(), true); break;
			case 1: spectrum->SetBackground(MakeRBSpectrum(), true); break;
			case 2: spectrum->SetBackground(MakeRGSpectrum(), true); break;
			case 3: spectrum->SetBackground(MakeHSSpectrum(), true); break;
			case 4: spectrum->SetBackground(MakeSVSpectrum(), true); break;
		}
	}

	switch (i) {
		case 0: case 1: case 2:
			slider->SetBackground(&rgb_slider[i]);
			slider->SetXY(0, rgb_input[i]->GetValue());
			spectrum->SetXY(rgb_input[2 - (i == 2)]->GetValue(), rgb_input[i == 0]->GetValue());
			break;
		case 3:
			slider->SetBackground(&hsl_slider);
			slider->SetXY(0, hsl_input[2]->GetValue());
			spectrum->SetXY(hsl_input[1]->GetValue(), hsl_input[0]->GetValue());
			break;
		case 4:
			slider->SetBackground(&hsv_slider);
			slider->SetXY(0, hsv_input[0]->GetValue());
			spectrum->SetXY(hsv_input[1]->GetValue(), hsv_input[2]->GetValue());
			break;
	}
	spectrum_dirty = false;

	wxBitmap tempBmp = preview_box->GetBitmap();
	{
		wxMemoryDC previewdc;
		previewdc.SelectObject(tempBmp);
		previewdc.SetPen(*wxTRANSPARENT_PEN);
		previewdc.SetBrush(wxBrush(to_wx(cur_color)));
		previewdc.DrawRectangle(0, 0, 40, 40);
	}
	preview_box->SetBitmap(tempBmp);

	alpha_slider_img = make_slider_img([this](unsigned char *slid) {
		static_assert(slider_width % alpha_box_size == 0, "Slider width must be a multiple of alpha box width");

		for (int y = 0; y < 256; ++y) {
			unsigned char inv_y = 0xFF - y;

			unsigned char box_colors[] = {
				static_cast<unsigned char>(0x66 - inv_y * 0x66 / 0xFF),
				static_cast<unsigned char>(0x99 - inv_y * 0x99 / 0xFF)
			};

			if ((y / alpha_box_size) & 1)
				std::swap(box_colors[0], box_colors[1]);

			unsigned char colors[2][3];
			for (int i = 0; i < 2; ++i) {
				colors[i][0] = cur_color.r * inv_y / 0xFF + box_colors[i];
				colors[i][1] = cur_color.g * inv_y / 0xFF + box_colors[i];
				colors[i][2] = cur_color.b * inv_y / 0xFF + box_colors[i];
			}

			for (int x = 0; x < slider_width; ++x) {
				*slid++ = colors[x / alpha_box_size][0];
				*slid++ = colors[x / alpha_box_size][1];
				*slid++ = colors[x / alpha_box_size][2];
			}
		}
	});
	alpha_slider->SetBackground(&alpha_slider_img, true);

	callback(cur_color);
}

template<typename Func>
static wxBitmap *make_spectrum(wxBitmap *bitmap, Func func) {
	wxImage spectrum_image(256, 256);
	func(spectrum_image.GetData());
	*bitmap = wxBitmap(spectrum_image);
	return bitmap;
}

wxBitmap *DialogColorPicker::MakeGBSpectrum() {
	return make_spectrum(&rgb_spectrum[0], [this](unsigned char *spec) {
		for (int g = 0; g < 256; g++) {
			for (int b = 0; b < 256; b++) {
				*spec++ = cur_color.r;
				*spec++ = g;
				*spec++ = b;
			}
		}
	});
}

wxBitmap *DialogColorPicker::MakeRBSpectrum() {
	return make_spectrum(&rgb_spectrum[1], [this](unsigned char *spec) {
		for (int r = 0; r < 256; r++) {
			for (int b = 0; b < 256; b++) {
				*spec++ = r;
				*spec++ = cur_color.g;
				*spec++ = b;
			}
		}
	});
}

wxBitmap *DialogColorPicker::MakeRGSpectrum() {
	return make_spectrum(&rgb_spectrum[2], [this](unsigned char *spec) {
		for (int r = 0; r < 256; r++) {
			for (int g = 0; g < 256; g++) {
				*spec++ = r;
				*spec++ = g;
				*spec++ = cur_color.b;
			}
		}
	});
}

wxBitmap *DialogColorPicker::MakeHSSpectrum() {
	int l = hsl_input[2]->GetValue();
	return make_spectrum(&hsl_spectrum, [=](unsigned char *spec) {
		for (int h = 0; h < 256; h++) {
			unsigned char maxr, maxg, maxb;
			hsl_to_rgb(h, 255, l, &maxr, &maxg, &maxb);

			for (int s = 0; s < 256; s++) {
				*spec++ = maxr * s / 256 + (255-s) * l / 256;
				*spec++ = maxg * s / 256 + (255-s) * l / 256;
				*spec++ = maxb * s / 256 + (255-s) * l / 256;
			}
		}
	});
}

wxBitmap *DialogColorPicker::MakeSVSpectrum() {
	int h = hsv_input[0]->GetValue();
	unsigned char maxr, maxg, maxb;
	hsv_to_rgb(h, 255, 255, &maxr, &maxg, &maxb);

	return make_spectrum(&hsv_spectrum, [=](unsigned char *spec) {
		for (int v = 0; v < 256; v++) {
			int rr = (255-maxr) * v / 256;
			int rg = (255-maxg) * v / 256;
			int rb = (255-maxb) * v / 256;
			for (int s = 0; s < 256; s++) {
				*spec++ = 255 - rr * s / 256 - (255-v);
				*spec++ = 255 - rg * s / 256 - (255-v);
				*spec++ = 255 - rb * s / 256 - (255-v);
			}
		}
	});
}

void DialogColorPicker::OnChangeMode(wxCommandEvent &) {
	spectrum_dirty = true;
	OPT_SET("Tool/Colour Picker/Mode")->SetInt(colorspace_choice->GetSelection());
	UpdateSpectrumDisplay();
}

void DialogColorPicker::OnSpectrumChange(wxCommandEvent &) {
	int i = colorspace_choice->GetSelection();
	switch (i) {
		case 0: case 1: case 2:
			change_value(rgb_input[2 - (i == 2)], spectrum->GetX());
			change_value(rgb_input[i == 0], spectrum->GetY());
			break;
		case 3:
			change_value(hsl_input[1], spectrum->GetX());
			change_value(hsl_input[0], spectrum->GetY());
			break;
		case 4:
			change_value(hsv_input[1], spectrum->GetX());
			change_value(hsv_input[2], spectrum->GetY());
			break;
	}

	switch (i) {
		case 0: case 1: case 2:
			UpdateFromRGB(false);
			break;
		case 3:
			UpdateFromHSL(false);
			break;
		case 4:
			UpdateFromHSV(false);
			break;
	}
}

void DialogColorPicker::OnSliderChange(wxCommandEvent &) {
	spectrum_dirty = true;
	int i = colorspace_choice->GetSelection();
	switch (i) {
		case 0: case 1: case 2:
			change_value(rgb_input[i], slider->GetY());
			UpdateFromRGB(false);
			break;
		case 3:
			change_value(hsl_input[2], slider->GetY());
			UpdateFromHSL(false);
			break;
		case 4:
			change_value(hsv_input[0], slider->GetY());
			UpdateFromHSV(false);
			break;
	}
}

void DialogColorPicker::OnAlphaSliderChange(wxCommandEvent &) {
	change_value(alpha_input, alpha_slider->GetY());
	cur_color.a = alpha_slider->GetY();
	callback(cur_color);
}

void DialogColorPicker::OnRecentSelect(ValueEvent<agi::Color> &evt) {
	agi::Color new_color = evt.Get();
	new_color.a = cur_color.a;
	SetColor(new_color);
}

void DialogColorPicker::OnDropperMouse(wxMouseEvent &evt) {
	if (evt.LeftDown() && !screenshot_screen_dropper_icon->HasCapture()) {
#ifdef WIN32
		screenshot_screen_dropper_icon->SetCursor(wxCursor("eyedropper_cursor"));
#else
		screenshot_screen_dropper_icon->SetCursor(*wxCROSS_CURSOR);
#endif
		screenshot_screen_dropper_icon->SetBitmap(wxNullBitmap);
		screenshot_screen_dropper_icon->CaptureMouse();
		eyedropper_grab_point = evt.GetPosition();
		eyedropper_is_grabbed = false;
	}

	if (evt.LeftUp()) {
		wxPoint ptdiff = evt.GetPosition() - eyedropper_grab_point;
		bool release_now = eyedropper_is_grabbed || abs(ptdiff.x) + abs(ptdiff.y) > 7;
		if (release_now) {
			screenshot_screen_dropper_icon->ReleaseMouse();
			eyedropper_is_grabbed = false;
			screenshot_screen_dropper_icon->SetCursor(wxNullCursor);
			screenshot_screen_dropper_icon->SetBitmap(eyedropper_bitmap);
		}
		else
			eyedropper_is_grabbed = true;
	}

	if (screenshot_screen_dropper_icon->HasCapture()) {
		wxPoint scrpos = screenshot_screen_dropper_icon->ClientToScreen(evt.GetPosition());
		screenshot_screen_dropper->DropFromScreenXY(scrpos.x, scrpos.y);
	}
}

/// @brief Hack to redirect events to the screen dropper icon
void DialogColorPicker::OnMouse(wxMouseEvent &evt) {
	// this handler is only registered if screenshot_screen_dropper is enabled
	if (!screenshot_screen_dropper_icon->HasCapture()) {
		evt.Skip();
		return;
	}

	wxPoint dropper_pos = screenshot_screen_dropper_icon->ScreenToClient(ClientToScreen(evt.GetPosition()));
	evt.m_x = dropper_pos.x;
	evt.m_y = dropper_pos.y;
	screenshot_screen_dropper_icon->GetEventHandler()->ProcessEvent(evt);
}

void DialogColorPicker::OnCaptureLost(wxMouseCaptureLostEvent&) {
	eyedropper_is_grabbed = false;
	screenshot_screen_dropper_icon->SetCursor(wxNullCursor);
	screenshot_screen_dropper_icon->SetBitmap(eyedropper_bitmap);
}

#ifdef WITH_LIBPORTAL

namespace {
unsigned char float_to_byte(double x) {
	if (std::isnan(x) || x <= 0.0) return 0;
	if (x >= 1.0) return 255;
	return static_cast<unsigned char>(x * 255 + 0.5);
	// NOTE: I have encountered cases where the returned value was off by one,
	//       probably due to incorrect rounding. However, I am unable to reproduce it.
}

void PortalPickColorCallback(GObject *source, GAsyncResult *res, gpointer data) {
	// this will be called from GLib main loop, which likely runs on wxWidgets main thread (at least on wxGTK), but don't rely on it
	wxEvtHandler *event_target = static_cast<wxEvtHandler *>(data);
	GError *raw_error = nullptr;
	std::unique_ptr<GVariant, decltype(&g_variant_unref)> color_result{xdp_portal_pick_color_finish(XDP_PORTAL(source), res, &raw_error), g_variant_unref};
	// FIXME: Use out_ptr here once we can use C++23
	std::unique_ptr<GError, decltype(&g_error_free)> error{raw_error, g_error_free};

	if (error) {
		if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			LOG_D("dialog_colorpicker") << "XDG Desktop Portal PickColor cancelled";
		} else {
			LOG_W("dialog_colorpicker") << "XDG Desktop Portal PickColor failed: " << error->message;
			// TODO inform the user about the error
		}
		return;
	}

	gdouble r, g, b;
	g_variant_get(color_result.get(), "(ddd)", &r, &g, &b);
	agi::Color color(float_to_byte(r), float_to_byte(g), float_to_byte(b), 0);
	// FIXME what if event_target has been destroyed in the meantime? (unlikely, but cannot be completely ruled out)
	event_target->QueueEvent(new ValueEvent<agi::Color>(EVT_OS_SELECT, 0, color));
}
}

void DialogColorPicker::OnOsDropperClick(wxCommandEvent&) {
	auto parent = agi::xdp_utils::GetXdpParent(this);
	xdp_portal_pick_color(agi::xdp_utils::portal, parent.get(), nullptr, PortalPickColorCallback, static_cast<wxEvtHandler *>(this));
}

#else // WITH_LIBPORTAL

void DialogColorPicker::OnOsDropperClick(wxCommandEvent&) {
	throw agi::InternalError("unimplemented");
}

#endif // WITH_LIBPORTAL

}

bool GetColorFromUser(wxWindow* parent, agi::Color original, bool alpha, std::function<void (agi::Color)> callback) {
	DialogColorPicker dialog(parent, original, callback, alpha);
	bool ok = dialog.ShowModal() == wxID_OK;
	if (!ok)
		callback(original);
	else
		dialog.AddColorToRecent();
	return ok;
}
