You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
505 lines
12 KiB
505 lines
12 KiB
/* |
|
* 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; |
|
}
|
|
|