view libmlk-rpg/mlk/rpg/message.c @ 646:7e1eb7f6c049 default tip @

misc: remove .clang
author David Demelier <markand@malikania.fr>
date Sun, 04 Feb 2024 15:24:37 +0100
parents e16808365d42
children
line wrap: on
line source

/*
 * message.c -- message dialog
 *
 * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <assert.h>
#include <stdlib.h>
#include <string.h>

#include <mlk/core/err.h>
#include <mlk/core/event.h>
#include <mlk/core/font.h>
#include <mlk/core/maths.h>
#include <mlk/core/painter.h>
#include <mlk/core/panic.h>
#include <mlk/core/sprite.h>
#include <mlk/core/texture.h>
#include <mlk/core/trace.h>
#include <mlk/core/util.h>

#include <mlk/ui/align.h>
#include <mlk/ui/ui.h>
#include <mlk/ui/ui_p.h>

#include "message.h"

static inline int
is_selectable(const struct mlk_message *msg, size_t n)
{
	return msg->lines[n] && ((msg->selectable >> n) & 0x1) == 1;
}

static inline struct mlk_message_style *
get_style(struct mlk_message *msg)
{
	return msg->style ? msg->style : mlk_message_style;
}

static inline struct mlk_font *
get_font(struct mlk_message *msg)
{
	return MLK__STYLE_FONT(get_style(msg)->font, MLK_UI_FONT_INTERFACE);
}

static unsigned int
min_width(struct mlk_message_style *style, struct mlk_message *msg)
{
	struct mlk_font *font;
	unsigned int maxw = 0, w = 0;
	int err;

	font = MLK__STYLE_FONT(style->font, MLK_UI_FONT_INTERFACE);

	for (size_t i = 0; i < msg->linesz; ++i) {
		if (!msg->lines[i])
			continue;
		if ((err = mlk_font_query(font, msg->lines[i], &w, NULL)) < 0)
			return err;
		if (w > maxw)
			maxw = w;
	}

	return (style->padding * 2) + maxw;
}

static unsigned int
min_height(struct mlk_message_style *style, struct mlk_message *msg)
{
	assert(msg);

	struct mlk_font *font;
	unsigned int lh;

	font = MLK__STYLE_FONT(style->font, MLK_UI_FONT_INTERFACE);
	lh = mlk_font_height(font);

	return (style->padding * 2) + (msg->linesz * lh) + ((msg->linesz - 1) * style->padding);
}

static void
draw_frame(struct mlk_message *msg)
{
	struct mlk_message_style *style = get_style(msg);

	mlk_painter_set_color(style->border);
	mlk_painter_draw_rectangle(0, 0, msg->w, msg->h);
	mlk_painter_set_color(style->background);
	mlk_painter_draw_rectangle(
		style->border_size,
		style->border_size,
		msg->w - (style->border_size * 2),
		msg->h - (style->border_size * 2)
	);
}

static void
draw_lines(struct mlk_message *msg)
{
	struct mlk_message_style *style;
	struct mlk_font *font;
	struct mlk_texture texture;
	unsigned long color;
	int x, y;

	style = get_style(msg);
	font = get_font(msg);

	for (size_t i = 0; i < msg->linesz; ++i) {
		if (!msg->lines[i])
			continue;

		if (msg->selectable && msg->selected == i && is_selectable(msg, i))
			color = style->color_selected;
		else
			color = style->color;

		if (mlk_font_render(font, &texture, msg->lines[i], color) < 0) {
			mlk_tracef("unable to render message text", mlk_err());
			continue;
		}

		x = style->padding;
		y = style->padding + (i * (texture.h + style->padding));

		if (x + texture.w > msg->w)
			mlk_tracef("message width too small: %u < %u", msg->w, min_width(style, msg));
		if (y + texture.h > msg->h)
			mlk_tracef("message height too small: %u < %u", msg->h, min_height(style, msg));

		mlk_texture_draw(&texture, x, y);
		mlk_texture_finish(&texture);
	}
}

static int
query(struct mlk_message_style *self,
      struct mlk_message *msg,
      unsigned int *w,
      unsigned int *h)
{
	if (w)
		*w = min_width(self, msg);
	if (h)
		*h = min_height(self, msg);

	return 0;
}

static void
update(struct mlk_message_style *self,
       struct mlk_message *msg,
       unsigned int ticks)
{
	(void)self;

	const struct mlk_message_style *style = MLK__STYLE(msg, mlk_message_style);

	msg->elapsed += ticks;

	switch (msg->state) {
	case MLK_MESSAGE_STATE_OPENING:
		msg->scale = (double)msg->elapsed / (double)style->speed;

		if (msg->scale > 1)
			msg->scale = 1;

		if (msg->elapsed >= style->speed) {
			msg->state = MLK_MESSAGE_STATE_SHOWING;
			msg->elapsed = 0;
		}

		break;
	case MLK_MESSAGE_STATE_SHOWING:
		/* Do automatically switch state if requested by the user. */
		if (msg->flags & MLK_MESSAGE_FLAGS_AUTOMATIC && msg->elapsed >= style->timeout) {
			msg->state = msg->flags & MLK_MESSAGE_FLAGS_FADEOUT
			    ? MLK_MESSAGE_STATE_HIDING
			    : MLK_MESSAGE_STATE_NONE;
			msg->elapsed = 0;
		}

		break;
	case MLK_MESSAGE_STATE_HIDING:
		msg->scale = 1 - (double)msg->elapsed / (double)style->speed;

		if (msg->scale < 0)
			msg->scale = 0;
		if (msg->elapsed >= style->speed) {
			msg->state = MLK_MESSAGE_STATE_NONE;
			msg->elapsed = 0;
		}

		break;
	default:
		break;
	}

}

static void
draw(struct mlk_message_style *self, struct mlk_message *msg)
{
	(void)self;

	struct mlk_texture tex;
	int x, y;
	unsigned int w, h;

	if (msg->w == 0 || msg->h == 0) {
		mlk_tracef("message has null dimensions");
		return;
	}

	if (mlk_texture_init(&tex, msg->w, msg->h) < 0)
		mlk_panic();

	MLK_PAINTER_BEGIN(&tex);
	draw_frame(msg);
	draw_lines(msg);
	MLK_PAINTER_END();

	/* Compute scaling. */
	w = msg->w * msg->scale;
	h = msg->h * msg->scale;

	/* Centerize within its drawing area. */
	mlk_align(MLK_ALIGN_CENTER, &x, &y, w, h, msg->x, msg->y, msg->w, msg->h);

	/* Draw and clear. */
	mlk_texture_scale(&tex, 0, 0, msg->w, msg->h, x, y, w, h, 0);
	mlk_texture_finish(&tex);
}

// TODO: add dark variant.
struct mlk_message_style mlk_message_style_dark = {
	.background     = MLK_UI_COLOR_BG,
	.border         = MLK_UI_COLOR_BORDER,
	.border_size    = MLK_UI_BORDER,
	.color          = MLK_UI_COLOR_TEXT,
	.color_selected = MLK_UI_COLOR_SELECTED,
	.padding        = MLK_UI_PADDING,
	.timeout        = MLK_MESSAGE_TIMEOUT_DEFAULT,
	.speed          = MLK_MESSAGE_SPEED_DEFAULT,
	.query          = query,
	.update         = update,
	.draw           = draw
};

struct mlk_message_style mlk_message_style_light = {
	.background     = MLK_UI_COLOR_BG,
	.border         = MLK_UI_COLOR_BORDER,
	.border_size    = MLK_UI_BORDER,
	.color          = MLK_UI_COLOR_TEXT,
	.color_selected = MLK_UI_COLOR_SELECTED,
	.padding        = MLK_UI_PADDING,
	.timeout        = MLK_MESSAGE_TIMEOUT_DEFAULT,
	.speed          = MLK_MESSAGE_SPEED_DEFAULT,
	.query          = query,
	.update         = update,
	.draw           = draw
};

struct mlk_message_style *mlk_message_style = &mlk_message_style_light;

void
mlk_message_start(struct mlk_message *msg)
{
	assert(msg);

	struct mlk_message_style *style = MLK__STYLE(msg, mlk_message_style);

	if ((msg->flags & (MLK_MESSAGE_FLAGS_FADEIN | MLK_MESSAGE_FLAGS_FADEOUT)) && style->speed == 0)
		mlk_tracef("message has animation but zero delay");

	msg->elapsed = 0;
	msg->scale = msg->flags & MLK_MESSAGE_FLAGS_FADEIN ? 0.0 : 1.0;
	msg->state = msg->flags & MLK_MESSAGE_FLAGS_FADEIN
	    ? MLK_MESSAGE_STATE_OPENING
	    : MLK_MESSAGE_STATE_SHOWING;

	if (msg->flags & MLK_MESSAGE_FLAGS_AUTOMATIC && style->timeout == 0)
		mlk_tracef("message is automatic but has zero timeout");

	/*
	 * Make sure selected index goes in the range of the lines and that it
	 * starts on a proper selectable line.
	 */
	if (msg->selectable) {
		if (msg->selected >= msg->linesz || !is_selectable(msg, msg->selected))
			msg->selected = 0;

		while (!is_selectable(msg, msg->selected))
			msg->selected++;
	}
}

int
mlk_message_query(struct mlk_message *msg, unsigned int *w, unsigned int *h)
{
	assert(msg);

	return MLK__STYLE_CALL(msg->style, mlk_message_style, query, msg, w, h);
}

static inline size_t
first(const struct mlk_message *msg)
{
	size_t ret = -1;

	for (size_t i = 0; i < msg->linesz; ++i) {
		if (is_selectable(msg, i)) {
			ret = i;
			break;
		}
	}

	return ret;
}

static inline size_t
last(const struct mlk_message *msg)
{
	size_t ret = -1;

	for (size_t i = msg->linesz; i >= 0; --i) {
		if (is_selectable(msg, i)) {
			ret = i;
			break;
		}
	}

	return ret;
}

static inline size_t
previous(const struct mlk_message *msg)
{
	size_t ret;

	/* wrap */
	if (msg->selected == first(msg))
		ret = last(msg);
	else {
		ret = msg->selected - 1;

		while (ret > 0 && !is_selectable(msg, ret))
			ret--;
	}

	return ret;
}

static inline size_t
next(const struct mlk_message *msg)
{
	size_t ret;

	/* wrap */
	if (msg->selected == last(msg))
		ret = first(msg);
	else {
		ret = msg->selected + 1;

		while (ret < msg->linesz && !is_selectable(msg, ret))
			ret++;
	}

	return ret;
}

void
mlk_message_handle(struct mlk_message *msg, const union mlk_event *ev)
{
	assert(msg);
	assert(ev);

	/* Skip if the message animation hasn't complete. */
	if (msg->state != MLK_MESSAGE_STATE_SHOWING)
		return;

	/* Only keyboard event are valid. */
	if (ev->type != MLK_EVENT_KEYDOWN || msg->state == MLK_MESSAGE_STATE_NONE)
		return;

	switch (ev->key.key) {
	case MLK_KEY_UP:
		if (msg->selectable && msg->linesz)
			msg->selected = previous(msg);
		break;
	case MLK_KEY_DOWN:
		if (msg->selectable && msg->linesz)
			msg->selected = next(msg);
		break;
	case MLK_KEY_ENTER:
		msg->state = msg->flags & MLK_MESSAGE_FLAGS_FADEOUT
		    ? MLK_MESSAGE_STATE_HIDING
		    : MLK_MESSAGE_STATE_NONE;
		msg->elapsed = 0;
		break;
	default:
		break;
	}
}

int
mlk_message_update(struct mlk_message *msg, unsigned int ticks)
{
	assert(msg);

	MLK__STYLE_CALL(msg->style, mlk_message_style, update, msg, ticks);

	return msg->state == MLK_MESSAGE_STATE_NONE;
}

void
mlk_message_draw(struct mlk_message *msg)
{
	assert(msg);

	MLK__STYLE_CALL(msg->style, mlk_message_style, draw, msg);
}

void
mlk_message_hide(struct mlk_message *msg)
{
	assert(msg);

	msg->state = MLK_MESSAGE_STATE_HIDING;
	msg->elapsed = 0;
}