Skip to content

annotate : add eraser binding - #345

Closed
AKArien wants to merge 1 commit into
WayfireWM:masterfrom
AKArien:annotate-erase
Closed

AKArien wants to merge 1 commit into
WayfireWM:masterfrom
AKArien:annotate-erase

Conversation

@AKArien

@AKArien AKArien commented Sep 1, 2026

Copy link
Copy Markdown

The eraser mode has it’s own keybind and line width, and uses the same drawing method as the drawing.

The drawing code has been significantly reworked to share code between erasing, drawing and previewing changes for shapes.

Disclosure : an LLM was quite extensively used for the cairo code, in part to get it done despite my inexperience with it. I have read and understand the code to the best of my abilities and take full responsability if something’s wrong.

The eraser mode has it’s own keybind and line width, and uses the same drawing
method as the drawing.

The drawing code has been significantly reworked to share code between erasing,
drawing and previewing changes for shapes.
@soreau

soreau commented Oct 17, 2026

Copy link
Copy Markdown
Member

The eraser mode has it’s own keybind and line width, and uses the same drawing method as the drawing.

This sounds like a fun idea.

The drawing code has been significantly reworked to share code between erasing, drawing and previewing changes for shapes.

This is not acceptable. A rework or rewrite should be a non-goal when adding features. 'Significant rework' is definitely out-of-scope here.

Disclosure : an LLM was quite extensively used for the cairo code, in part to get it done despite my inexperience with it. I have read and understand the code to the best of my abilities and take full responsability if something’s wrong.

I appreciate your effort here but there are far too many changes to 'get it done' in this PR.

I decided to try my hand at this to discover the subtleties and caveats. No AI or LLM was used and it took me about an hour. The block in the frame damage function could probably live in a separate function. Disclaimer: I am by no means a cairo expert.

diff --git a/metadata/annotate.xml b/metadata/annotate.xml
index e5e790b..59595c5 100644
--- a/metadata/annotate.xml
+++ b/metadata/annotate.xml
@@ -9,6 +9,11 @@
 			<_long>Begin drawing on workspace.</_long>
 			<default>&lt;super&gt; &lt;alt&gt; BTN_LEFT</default>
 		</option>
+		<option name="erase" type="button">
+			<_short>Erase</_short>
+			<_long>Erase previous renderings.</_long>
+			<default>&lt;super&gt; &lt;alt&gt; BTN_RIGHT</default>
+		</option>
 		<option name="clear_workspace" type="activator">
 			<_short>Clear Workspace</_short>
 			<_long>Clear workspace.</_long>
diff --git a/src/annotate.cpp b/src/annotate.cpp
index 3b052fd..09e77cc 100644
--- a/src/annotate.cpp
+++ b/src/annotate.cpp
@@ -25,7 +25,7 @@
 #include <math.h>
 #include <memory>
 #include <cairo.h>
-#include <wayfire/workspace-set.hpp> // IWYU pragma: keep
+#include <wayfire/workspace-set.hpp>
 #include "wayfire/geometry.hpp"
 #include "wayfire/opengl.hpp"
 #include "wayfire/region.hpp"
@@ -43,6 +43,7 @@
 
 enum annotate_draw_method
 {
+    ANNOTATE_METHOD_ERASE,
     ANNOTATE_METHOD_DRAW,
     ANNOTATE_METHOD_LINE,
     ANNOTATE_METHOD_RECTANGLE,
@@ -113,10 +114,8 @@ class simple_node_render_instance_t : public render_instance_t
 
         data.pass->custom_gles_subpass([&]
         {
-            wf::gles::bind_render_buffer(data.target);
-            for (auto& box : data.damage)
+            wf::gles::for_each_scissor_rect(data.target, data.damage, [&]
             {
-                wf::gles::render_target_logic_scissor(data.target, box);
                 if (ol->cr)
                 {
                     OpenGL::render_texture(wf::gles_texture_t{ol->texture->tex}, data.target, og,
@@ -128,7 +127,7 @@ class simple_node_render_instance_t : public render_instance_t
                     OpenGL::render_texture(wf::gles_texture_t{shape_overlay->texture->tex}, data.target, og,
                         glm::vec4(1.0), OpenGL::TEXTURE_TRANSFORM_INVERT_Y);
                 }
-            }
+            });
         });
     }
 };
@@ -197,7 +196,7 @@ std::shared_ptr<simple_node_t> add_simple_node(wf::output_t *output, int x, int
 
 class wayfire_annotate_screen : public wf::per_output_plugin_instance_t, public wf::pointer_interaction_t
 {
-    uint32_t button;
+    uint32_t draw_button, erase_button;
     wf::geometry_t last_bbox;
     annotate_draw_method draw_method;
     wf::pointf_t grab_point, last_cursor;
@@ -207,6 +206,7 @@ class wayfire_annotate_screen : public wf::per_output_plugin_instance_t, public
     wf::option_wrapper_t<bool> shapes_from_center{"annotate/from_center"};
     wf::option_wrapper_t<wf::color_t> stroke_color{"annotate/stroke_color"};
     wf::option_wrapper_t<wf::buttonbinding_t> draw_binding{"annotate/draw"};
+    wf::option_wrapper_t<wf::buttonbinding_t> erase_binding{"annotate/erase"};
     wf::option_wrapper_t<wf::activatorbinding_t> clear_binding{
         "annotate/clear_workspace"};
     std::unique_ptr<wf::input_grab_t> input_grab;
@@ -245,6 +245,7 @@ class wayfire_annotate_screen : public wf::per_output_plugin_instance_t, public
         output->connect(&viewport_changed);
         method.set_callback(method_changed);
         output->add_button(draw_binding, &draw_begin);
+        output->add_button(erase_binding, &erase_begin);
         output->add_activator(clear_binding, &clear_workspace);
         input_grab = std::make_unique<wf::input_grab_t>(this->grab_interface.name, output, nullptr, this,
             nullptr);
@@ -253,10 +254,20 @@ class wayfire_annotate_screen : public wf::per_output_plugin_instance_t, public
 
     void handle_pointer_button(const wlr_pointer_button_event& event) override
     {
-        if ((event.button == button) && (event.state == WL_POINTER_BUTTON_STATE_RELEASED))
+        if (event.state != WL_POINTER_BUTTON_STATE_RELEASED)
+        {
+            return;
+        }
+
+        if (event.button == draw_button)
         {
             draw_end();
         }
+
+        if (event.button == erase_button)
+        {
+            erase_end();
+        }
     }
 
     wf::config::option_base_t::updated_callback_t method_changed = [=] ()
@@ -324,8 +335,21 @@ class wayfire_annotate_screen : public wf::per_output_plugin_instance_t, public
     {
         output->render->add_effect(&frame_pre_paint, wf::OUTPUT_EFFECT_DAMAGE);
         output->render->damage_whole();
-        grab_point = last_cursor = wf::get_core().get_cursor_position();
-        button     = btn.get_button();
+        grab_point  = last_cursor = wf::get_core().get_cursor_position();
+        draw_button = btn.get_button();
+
+        grab();
+
+        return false;
+    };
+
+    wf::button_callback erase_begin = [=] (wf::buttonbinding_t btn)
+    {
+        output->render->add_effect(&frame_pre_paint, wf::OUTPUT_EFFECT_DAMAGE);
+        last_cursor  = wf::get_core().get_cursor_position();
+        draw_method  = ANNOTATE_METHOD_ERASE;
+        erase_button = btn.get_button();
+        output->render->damage_whole();
 
         grab();
 
@@ -360,6 +384,13 @@ class wayfire_annotate_screen : public wf::per_output_plugin_instance_t, public
         }
     }
 
+    void erase_end()
+    {
+        output->render->rem_effect(&frame_pre_paint);
+        ungrab();
+        method_changed();
+    }
+
     void overlay_clear(std::shared_ptr<anno_ws_overlay> ol)
     {
         if (!ol->cr)
@@ -493,6 +524,30 @@ class wayfire_annotate_screen : public wf::per_output_plugin_instance_t, public
         });
     }
 
+    void cairo_erase(std::shared_ptr<anno_ws_overlay> ol, wf::pointf_t to)
+    {
+        auto og = output->get_layout_geometry();
+
+        to.x -= og.x;
+        to.y -= og.y;
+
+        cairo_init(ol);
+        cairo_t *cr = ol->cr;
+
+        cairo_set_line_width(cr, line_width);
+
+        auto size = line_width * 2;
+
+        cairo_set_source_rgba(cr, 0, 0, 0, 0);
+        cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
+        cairo_rectangle(cr, to.x - size / 2, to.y - size / 2, size, size);
+        cairo_fill(cr);
+        wf::geometry_t bbox{to.x - size / 2 - 1, to.y - size / 2 - 1, size + 1, size + 1};
+        get_node_overlay()->do_push_damage(wf::regionf_t(bbox));
+        cairo_surface_upload_to_texture_with_damage(ol->cairo_surface, *ol->texture,
+            wf::to_integer_box(bbox));
+    }
+
     void cairo_draw(std::shared_ptr<anno_ws_overlay> ol, wf::pointf_t from,
         wf::pointf_t to)
     {
@@ -727,6 +782,40 @@ class wayfire_annotate_screen : public wf::per_output_plugin_instance_t, public
 
         switch (draw_method)
         {
+          case ANNOTATE_METHOD_ERASE:
+            if (ol && ol->texture)
+            {
+                auto size = line_width * 2;
+                static std::vector<unsigned char> clear_brush(size * size * 4, 0);
+                glBindTexture(GL_TEXTURE_2D, ol->texture->tex);
+
+                wf::pointf_t step;
+                std::vector<float> points;
+                auto d = glm::distance(glm::vec2(last_cursor.x, last_cursor.y),
+                    glm::vec2(current_cursor.x, current_cursor.y));
+
+                /* Interpolate between last and current cursor */
+                int num_points = std::min(int(d / 5 + 1), 64);
+                step.x = (current_cursor.x - last_cursor.x) / num_points;
+                step.y = (current_cursor.y - last_cursor.y) / num_points;
+                for (int i = 0; i < num_points; i++)
+                {
+                    wf::pointf_t p = wf::pointf_t{
+                        current_cursor.x - step.x * i,
+                        current_cursor.y - step.y * i};
+                    glTexSubImage2D(GL_TEXTURE_2D, 0,
+                        p.x - size / 2, p.y - size / 2,
+                        size, size, GL_RGBA, GL_UNSIGNED_BYTE,
+                        clear_brush.data());
+                    cairo_erase(ol, p);
+                    output->render->damage({p.x - size / 2, p.y - size / 2, size, size});
+                }
+
+                glBindTexture(GL_TEXTURE_2D, 0);
+            }
+
+            break;
+
           case ANNOTATE_METHOD_DRAW:
             cairo_draw(ol, last_cursor, current_cursor);
             break;

@soreau

soreau commented Oct 17, 2026

Copy link
Copy Markdown
Member

Thank you for your idea, it is implemented in #356. I was able to simplify the code even further than the patch above. I also added a Co-authored-by-you tag to the commit message. Please try the annotate-eraser branch and let me know if it meets your expectations.

@soreau soreau closed this Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants