|
|
|
/*
|
|
|
|
* Copyright (C) 2020 Alexandros Theodotou <alex at zrythm dot org>
|
|
|
|
*
|
|
|
|
* This file is part of ZToolkit
|
|
|
|
*
|
|
|
|
* ZToolkit is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as
|
|
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* ZToolkit is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Affero Public License
|
|
|
|
* along with ZToolkit. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
#include "ztoolkit/ztk.h"
|
|
|
|
|
|
|
|
/** Height of the separator, excluding padding. */
|
|
|
|
#define SEPARATOR_HEIGHT 2
|
|
|
|
|
|
|
|
/** Padding to leave left/right/bot/top of the
|
|
|
|
* text. */
|
|
|
|
#define PADDING 2
|
|
|
|
|
|
|
|
static double
|
|
|
|
get_height (
|
|
|
|
ZtkComboBox * self)
|
|
|
|
{
|
|
|
|
double height = PADDING * 2;
|
|
|
|
for (int i = 0; i < self->num_elements; i++)
|
|
|
|
{
|
|
|
|
ZtkComboBoxElement * el = &self->elements[i];
|
|
|
|
if (el->is_separator)
|
|
|
|
{
|
|
|
|
height += SEPARATOR_HEIGHT + PADDING * 2;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
/* the app is not set until the widget gets
|
|
|
|
* added to the app */
|
|
|
|
ZtkWidget * widget = (ZtkWidget *) self;
|
|
|
|
if (!widget->app)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
cairo_t* cr =
|
|
|
|
(cairo_t*) puglGetContext (
|
|
|
|
widget->app->view);
|
|
|
|
cairo_text_extents_t extents;
|
|
|
|
cairo_set_font_size (cr, self->font_size);
|
|
|
|
cairo_text_extents (cr, el->label, &extents);
|
|
|
|
|
|
|
|
height += (int) extents.height + PADDING * 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return height;
|
|
|
|
}
|
|
|
|
|
|
|
|
static double
|
|
|
|
get_width (
|
|
|
|
ZtkComboBox * self)
|
|
|
|
{
|
|
|
|
double width = 12;
|
|
|
|
for (int i = 0; i < self->num_elements; i++)
|
|
|
|
{
|
|
|
|
ZtkComboBoxElement * el = &self->elements[i];
|
|
|
|
if (el->is_separator)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
/* the app is not set until the widget gets
|
|
|
|
* added to the app */
|
|
|
|
ZtkWidget * widget = (ZtkWidget *) self;
|
|
|
|
if (!widget->app)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
cairo_t* cr =
|
|
|
|
(cairo_t*) puglGetContext (
|
|
|
|
widget->app->view);
|
|
|
|
cairo_text_extents_t extents;
|
|
|
|
cairo_set_font_size (cr, self->font_size);
|
|
|
|
cairo_text_extents (cr, el->label, &extents);
|
|
|
|
|
|
|
|
width =
|
|
|
|
/* *2 for the element, *2 for the frame */
|
|
|
|
MAX (width, extents.width + PADDING * 4);
|
|
|
|
}
|
|
|
|
|
|
|
|
return width;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
get_dimensions (
|
|
|
|
ZtkComboBox * self,
|
|
|
|
ZtkRect * rect)
|
|
|
|
{
|
|
|
|
double height = get_height (self);
|
|
|
|
double width = get_width (self);
|
|
|
|
rect->width = width;
|
|
|
|
rect->height = height;
|
|
|
|
if (self->upwards)
|
|
|
|
{
|
|
|
|
if (self->backwards)
|
|
|
|
{
|
|
|
|
rect->x =
|
|
|
|
(self->parent->rect.x +
|
|
|
|
self->parent->rect.width) - width;
|
|
|
|
rect->y = self->parent->rect.y - height;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
rect->x = self->parent->rect.x;
|
|
|
|
rect->y = self->parent->rect.y - height;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else /* downwards */
|
|
|
|
{
|
|
|
|
if (self->backwards)
|
|
|
|
{
|
|
|
|
rect->x =
|
|
|
|
(self->parent->rect.x +
|
|
|
|
self->parent->rect.width) - width;
|
|
|
|
rect->y =
|
|
|
|
self->parent->rect.y +
|
|
|
|
self->parent->rect.height;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
rect->x = self->parent->rect.x;
|
|
|
|
rect->y =
|
|
|
|
self->parent->rect.y +
|
|
|
|
self->parent->rect.height;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
draw_cb (
|
|
|
|
ZtkWidget * w,
|
|
|
|
cairo_t * cr,
|
|
|
|
ZtkRect * draw_rect,
|
|
|
|
void * data)
|
|
|
|
{
|
|
|
|
ZtkComboBox * self = (ZtkComboBox *) w;
|
|
|
|
|
|
|
|
ZtkRect rect;
|
|
|
|
get_dimensions (self, &rect);
|
|
|
|
|
|
|
|
/* draw bg and frame */
|
|
|
|
ztk_color_set_for_cairo (
|
|
|
|
&self->frame_color, cr);
|
|
|
|
cairo_rectangle (
|
|
|
|
cr, rect.x, rect.y, rect.width,
|
|
|
|
rect.height);
|
|
|
|
cairo_fill (cr);
|
|
|
|
ztk_color_set_for_cairo (
|
|
|
|
&self->bg_color, cr);
|
|
|
|
cairo_rectangle (
|
|
|
|
cr, rect.x + PADDING, rect.y + PADDING,
|
|
|
|
rect.width - PADDING * 2,
|
|
|
|
rect.height - PADDING * 2);
|
|
|
|
cairo_fill (cr);
|
|
|
|
|
|
|
|
double height = rect.y + PADDING;
|
|
|
|
double next_height;
|
|
|
|
|
|
|
|
/* find element hit */
|
|
|
|
for (int i = 0; i < self->num_elements; i++)
|
|
|
|
{
|
|
|
|
ZtkComboBoxElement * el = &self->elements[i];
|
|
|
|
if (el->is_separator)
|
|
|
|
{
|
|
|
|
next_height =
|
|
|
|
height + SEPARATOR_HEIGHT +
|
|
|
|
PADDING * 2;
|
|
|
|
|
|
|
|
/* draw the element bg */
|
|
|
|
if (self->hovered_idx == i)
|
|
|
|
{
|
|
|
|
if (w->state & ZTK_WIDGET_STATE_PRESSED)
|
|
|
|
{
|
|
|
|
ztk_color_set_for_cairo (
|
|
|
|
&self->click_color, cr);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ztk_color_set_for_cairo (
|
|
|
|
&self->hover_color, cr);
|
|
|
|
}
|
|
|
|
cairo_rectangle (
|
|
|
|
cr, rect.x + PADDING, height,
|
|
|
|
rect.width - PADDING * 2,
|
|
|
|
next_height - height);
|
|
|
|
cairo_fill (cr);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* draw the separator */
|
|
|
|
ztk_color_set_for_cairo (
|
|
|
|
&self->separator_color, cr);
|
|
|
|
cairo_rectangle (
|
|
|
|
cr, rect.x + PADDING * 2,
|
|
|
|
height + PADDING,
|
|
|
|
rect.width - PADDING * 4,
|
|
|
|
SEPARATOR_HEIGHT);
|
|
|
|
cairo_fill (cr);
|
|
|
|
|
|
|
|
height = next_height;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
cairo_text_extents_t extents;
|
|
|
|
cairo_set_font_size (cr, self->font_size);
|
|
|
|
cairo_text_extents (
|
|
|
|
cr, el->label, &extents);
|
|
|
|
next_height =
|
|
|
|
height + extents.height +
|
|
|
|
PADDING * 2;
|
|
|
|
|
|
|
|
/* draw the element bg */
|
|
|
|
if (self->hovered_idx == i)
|
|
|
|
{
|
|
|
|
if (w->state & ZTK_WIDGET_STATE_PRESSED)
|
|
|
|
{
|
|
|
|
ztk_color_set_for_cairo (
|
|
|
|
&self->click_color, cr);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ztk_color_set_for_cairo (
|
|
|
|
&self->hover_color, cr);
|
|
|
|
}
|
|
|
|
cairo_rectangle (
|
|
|
|
cr, rect.x + PADDING, height,
|
|
|
|
rect.width - PADDING * 2,
|
|
|
|
next_height - height);
|
|
|
|
cairo_fill (cr);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* draw the text */
|
|
|
|
if (self->hovered_idx == i)
|
|
|
|
{
|
|
|
|
if (w->state & ZTK_WIDGET_STATE_PRESSED)
|
|
|
|
{
|
|
|
|
ztk_color_set_for_cairo (
|
|
|
|
&self->text_click_color, cr);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ztk_color_set_for_cairo (
|
|
|
|
&self->text_hover_color, cr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ztk_color_set_for_cairo (
|
|
|
|
&self->text_normal_color, cr);
|
|
|
|
}
|
|
|
|
|
|
|
|
cairo_move_to (
|
|
|
|
cr, rect.x + PADDING * 2,
|
|
|
|
height + PADDING + extents.height);
|
|
|
|
cairo_show_text (
|
|
|
|
cr, el->label);
|
|
|
|
|
|
|
|
height = next_height;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
update_cb (
|
|
|
|
ZtkWidget * w,
|
|
|
|
void * data)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
motion_cb (
|
|
|
|
ZtkWidget * widget,
|
|
|
|
const PuglEventMotion * event,
|
|
|
|
void * data)
|
|
|
|
{
|
|
|
|
/* set hovered */
|
|
|
|
ZtkComboBox * self = (ZtkComboBox *) widget;
|
|
|
|
|
|
|
|
double y = event->y;
|
|
|
|
double height = widget->rect.y + PADDING;
|
|
|
|
double next_height;
|
|
|
|
|
|
|
|
/* find element hit */
|
|
|
|
for (int i = 0; i < self->num_elements; i++)
|
|
|
|
{
|
|
|
|
ZtkComboBoxElement * el = &self->elements[i];
|
|
|
|
if (el->is_separator)
|
|
|
|
{
|
|
|
|
next_height =
|
|
|
|
height + SEPARATOR_HEIGHT +
|
|
|
|
PADDING * 2;
|
|
|
|
if (y >= height && y < next_height)
|
|
|
|
{
|
|
|
|
self->hovered_idx = i;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
height = next_height;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
cairo_t* cr =
|
|
|
|
(cairo_t*) puglGetContext (
|
|
|
|
((ZtkWidget *) self)->app->view);
|
|
|
|
cairo_text_extents_t extents;
|
|
|
|
cairo_set_font_size (
|
|
|
|
cr, self->font_size);
|
|
|
|
cairo_text_extents (
|
|
|
|
cr, el->label, &extents);
|
|
|
|
next_height =
|
|
|
|
height + extents.height +
|
|
|
|
PADDING * 2;
|
|
|
|
if (y >= height && y < next_height)
|
|
|
|
{
|
|
|
|
self->hovered_idx = i;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
height = next_height;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
self->hovered_idx = -1;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
free_cb (
|
|
|
|
ZtkWidget * w,
|
|
|
|
void * data)
|
|
|
|
{
|
|
|
|
ZtkComboBox * self = (ZtkComboBox *) w;
|
|
|
|
|
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
button_event_cb (
|
|
|
|
ZtkWidget * widget,
|
|
|
|
const PuglEventButton * btn,
|
|
|
|
void * data)
|
|
|
|
{
|
|
|
|
ZtkComboBox * self = (ZtkComboBox *) widget;
|
|
|
|
|
|
|
|
const PuglEvent * ev = (const PuglEvent *) btn;
|
|
|
|
if (ztk_widget_is_hit (widget, btn->x, btn->y))
|
|
|
|
{
|
|
|
|
/* skip if already removed */
|
|
|
|
if (!ztk_app_contains_widget (
|
|
|
|
widget->app, widget))
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
/* skip if not release or no valid hover */
|
|
|
|
if (ev->type != PUGL_BUTTON_RELEASE ||
|
|
|
|
self->hovered_idx < 0)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
ZtkComboBoxElement * el =
|
|
|
|
&self->elements[self->hovered_idx];
|
|
|
|
if (!el->is_separator)
|
|
|
|
{
|
|
|
|
/* activate */
|
|
|
|
el->activate_cb (
|
|
|
|
widget, el->activate_cb_data);
|
|
|
|
|
|
|
|
/* remove from app to hide the
|
|
|
|
* combobox */
|
|
|
|
ztk_app_remove_widget (
|
|
|
|
widget->app, widget);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* clicked outside */
|
|
|
|
else
|
|
|
|
{
|
|
|
|
/* remove widget */
|
|
|
|
ztk_app_remove_widget (
|
|
|
|
widget->app, widget);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initializes the defaults.
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
ztk_combo_box_init (
|
|
|
|
ZtkComboBox * self)
|
|
|
|
{
|
|
|
|
strcpy (self->font_name, "Cantarrel");
|
|
|
|
self->font_size = 12.0;
|
|
|
|
ztk_color_parse_hex (
|
|
|
|
&self->text_normal_color, "#DDDDDD");
|
|
|
|
ztk_color_parse_hex (
|
|
|
|
&self->text_hover_color, "#EEEEEE");
|
|
|
|
ztk_color_parse_hex (
|
|
|
|
&self->text_click_color, "#FFFFFF");
|
|
|
|
ztk_color_parse_hex (
|
|
|
|
&self->bg_color, "#323232");
|
|
|
|
ztk_color_parse_hex (
|
|
|
|
&self->frame_color, "#646464");
|
|
|
|
ztk_color_parse_hex (
|
|
|
|
&self->separator_color, "#AAAAAA");
|
|
|
|
ztk_color_parse_hex (
|
|
|
|
&self->hover_color, "#646464");
|
|
|
|
ztk_color_parse_hex (
|
|
|
|
&self->click_color, "#868686");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new combobox.
|
|
|
|
*
|
|
|
|
* @param parent The parent widget to spawn on.
|
|
|
|
* @param spawn_upwards Spawn upwards instead of
|
|
|
|
* downards.
|
|
|
|
*/
|
|
|
|
ZtkComboBox *
|
|
|
|
ztk_combo_box_new (
|
|
|
|
ZtkWidget * parent,
|
|
|
|
int spawn_upwards,
|
|
|
|
int spawn_backwards)
|
|
|
|
{
|
|
|
|
ZtkComboBox * self =
|
|
|
|
calloc (1, sizeof (ZtkComboBox));
|
|
|
|
ZtkRect rect = { 0, 0, 0, 0 };
|
|
|
|
ztk_widget_init (
|
|
|
|
(ZtkWidget *) self, ZTK_WIDGET_TYPE_COMBO_BOX,
|
|
|
|
&rect, update_cb, draw_cb,
|
|
|
|
free_cb);
|
|
|
|
ZtkWidget * widget = (ZtkWidget *) self;
|
|
|
|
|
|
|
|
/* catch button events */
|
|
|
|
widget->button_event_cb = button_event_cb;
|
|
|
|
widget->motion_event_cb = motion_cb;
|
|
|
|
|
|
|
|
self->parent = parent;
|
|
|
|
self->upwards = spawn_upwards;
|
|
|
|
self->backwards = spawn_backwards;
|
|
|
|
|
|
|
|
ztk_combo_box_init (self);
|
|
|
|
|
|
|
|
self->hovered_idx = -1;
|
|
|
|
|
|
|
|
/* update dimensions */
|
|
|
|
get_dimensions (self, &widget->rect);
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param data Data related to the current element
|
|
|
|
* to pass to the activate callback.
|
|
|
|
*/
|
|
|
|
void
|
|
|
|
ztk_combo_box_add_text_element (
|
|
|
|
ZtkComboBox * self,
|
|
|
|
const char * label,
|
|
|
|
ZtkWidgetActivateCallback activate_cb,
|
|
|
|
void * data)
|
|
|
|
{
|
|
|
|
ZtkComboBoxElement * el =
|
|
|
|
&self->elements[self->num_elements++];
|
|
|
|
|
|
|
|
strcpy (el->label, label);
|
|
|
|
el->is_separator = 0;
|
|
|
|
el->activate_cb = activate_cb;
|
|
|
|
el->activate_cb_data = data;
|
|
|
|
|
|
|
|
/* update dimensions */
|
|
|
|
ZtkWidget * widget = (ZtkWidget *) self;
|
|
|
|
get_dimensions (self, &widget->rect);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
ztk_combo_box_add_separator (
|
|
|
|
ZtkComboBox * self)
|
|
|
|
{
|
|
|
|
ZtkComboBoxElement * el =
|
|
|
|
&self->elements[self->num_elements++];
|
|
|
|
el->is_separator = 1;
|
|
|
|
|
|
|
|
/* update dimensions */
|
|
|
|
ZtkWidget * widget = (ZtkWidget *) self;
|
|
|
|
get_dimensions (self, &widget->rect);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
ztk_combo_box_clear (
|
|
|
|
ZtkComboBox * self)
|
|
|
|
{
|
|
|
|
self->num_elements = 0;
|
|
|
|
}
|