From 5eba754c6027c0c39a84c7a20a7811f6837b85b6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 28 Jan 2021 17:32:13 +0530 Subject: [PATCH] Implement animation control escape codes --- docs/graphics-protocol.rst | 9 +++++++++ kitty/graphics.c | 39 ++++++++++++++++++++++++++++++-------- kitty_tests/graphics.py | 18 +++++++++++++++++- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/docs/graphics-protocol.rst b/docs/graphics-protocol.rst index 4bf4db71f..a5131b8fd 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -554,6 +554,15 @@ Key Value Default Description ``z`` 32-bit integer ``0`` The gap (in milliseconds) of this frame from the previous one. Values less than one are ignored, new frames are given a default gap of ``40ms`` if not specified. +**Keys for animation control** +----------------------------------------------------------- +``s`` Positive integer ``0`` ``1`` - start animation, ``>1`` - stop animation +``r`` Positive integer ``0`` The 1-based frame number of the frame that is being affected +``z`` 32-bit integer ``0`` The gap (in milliseconds) of this frame from the previous one. Values less than + one are ignored +``c`` Positive integer ``0`` The 1-based frame number of the frame that should be made the current frame + + **Keys for deleting images** ----------------------------------------------------------- ``d`` Single character. ``a`` What to delete. diff --git a/kitty/graphics.c b/kitty/graphics.c index f44c59fcf..b2f6c1c23 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -778,6 +778,7 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree #define _frame_number num_lines #define _other_frame_number num_cells #define _gap z_index +#define _animation_enabled data_width static Image* handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, Image *img, const uint8_t *payload) { @@ -923,6 +924,27 @@ handle_delete_frame_command(GraphicsManager *self, const GraphicsCommand *g, boo img->extra_framecnt--; return NULL; } + +static void +handle_animation_control_command(bool *is_dirty, const GraphicsCommand *g, Image *img) { + if (g->_frame_number) { + uint32_t frame_idx = g->_frame_number - 1; + if (frame_idx <= img->extra_framecnt) { + Frame *f = frame_idx ? img->extra_frames + frame_idx - 1 : &img->root_frame; + if (g->_gap > 0) f->gap = g->_gap; + } + } + if (g->_other_frame_number) { + uint32_t frame_idx = g->_other_frame_number - 1; + if (frame_idx != img->current_frame_index && frame_idx <= img->extra_framecnt) { + img->current_frame_index = frame_idx; + *is_dirty = true; + } + } + if (g->_animation_enabled) { + img->animation_enabled = g->_animation_enabled == 1; + } +} // }}} // Image lifetime/scrolling {{{ @@ -1181,6 +1203,8 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint if (ag.action == 'f') { img = handle_animation_frame_load_command(self, &ag, img, payload); ret = finish_command_response(&ag, img != NULL); + } else if (ag.action == 'a') { + handle_animation_control_command(is_dirty, &ag, img); } } break; @@ -1217,24 +1241,23 @@ new(PyTypeObject UNUSED *type, PyObject UNUSED *args, PyObject UNUSED *kwds) { static inline PyObject* image_as_dict(GraphicsManager *self, Image *img) { #define U(x) #x, img->x +#define B(x) #x, img->x ? Py_True : Py_False ImageAndFrame key = {.image_id = img->internal_id}; PyObject *frames = PyTuple_New(img->extra_framecnt); for (unsigned i = 0; i < img->extra_framecnt; i++) { key.frame_id = img->extra_frames[i].id; PyTuple_SET_ITEM(frames, i, Py_BuildValue( "{sI sI sN}", "gap", img->extra_frames[i].gap, "id", key.frame_id, "data", read_from_cache_python(self, key))); - if (PyErr_Occurred()) return NULL; + if (PyErr_Occurred()) { Py_CLEAR(frames); return NULL; } } key.frame_id = img->root_frame.id; - return Py_BuildValue("{sI sI sI sI sK sI sI sO sO sI sI sO sN sN}", + return Py_BuildValue("{sI sI sI sI sK sI sI sO sO sO sI sI sI sN sN}", U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(client_number), - "data_loaded", img->data_loaded ? Py_True : Py_False, - "is_4byte_aligned", img->is_4byte_aligned ? Py_True : Py_False, - U(current_frame_index), "root_frame_gap", img->root_frame.gap, - "animation_enabled", img->animation_enabled ? Py_True : Py_False, - "data", read_from_cache_python(self, key), - "extra_frames", frames + B(data_loaded), B(is_4byte_aligned), B(animation_enabled), + U(current_frame_index), "root_frame_gap", img->root_frame.gap, U(current_frame_index), + "data", read_from_cache_python(self, key), "extra_frames", frames ); +#undef B #undef U } diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py index e2c206db9..64367d2f2 100644 --- a/kitty_tests/graphics.py +++ b/kitty_tests/graphics.py @@ -635,11 +635,27 @@ class TestGraphics(BaseTest): {'gap': 40, 'id': 2, 'data': b'3' * 36}, {'gap': 101, 'id': 3, 'data': b'444444333333444444333333333333333333'}, )) + # test changing gaps + img = g.image_for_client_id(1) + self.assertEqual(img['root_frame_gap'], 40) + self.assertIsNone(li(a='a', i=1, r=1, z=13)) + img = g.image_for_client_id(1) + self.assertEqual(img['root_frame_gap'], 13) + self.assertIsNone(li(a='a', i=1, r=2, z=43)) + img = g.image_for_client_id(1) + self.assertEqual(img['extra_frames'][0]['gap'], 43) + # test changing current frame + img = g.image_for_client_id(1) + self.assertEqual(img['current_frame_index'], 0) + self.assertIsNone(li(a='a', i=1, c=2)) + img = g.image_for_client_id(1) + self.assertEqual(img['current_frame_index'], 1) + # test delete of frames t(payload='5' * 36, frame_number=4) img = g.image_for_client_id(1) self.assertEqual(img['extra_frames'], ( - {'gap': 40, 'id': 2, 'data': b'3' * 36}, + {'gap': 43, 'id': 2, 'data': b'3' * 36}, {'gap': 101, 'id': 3, 'data': b'444444333333444444333333333333333333'}, {'gap': 40, 'id': 4, 'data': b'5' * 36}, ))