diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js
index d172ee900234dc5778b1ac7590106589c139a152..ef461b1cbfc77ae222240ebfd25a4cf03a4965ab 100644
--- a/pd/nw/pdgui.js
+++ b/pd/nw/pdgui.js
@@ -2659,6 +2659,24 @@ function gui_gobj_deselect(cid, tag) {
     });
 }
 
+function gui_gobj_setdirty(cid, tag, state) {
+    var color;
+    switch (state) {
+        case 1:
+            color = "crimson";
+            break;
+        case 2:
+            color = "coral";
+            break;
+        default:
+            color = "none";
+            break;
+    }
+    gui(cid).get_elem(tag + "text", function(e) {
+        e.setAttribute("stroke", color);
+    });
+}
+
 function gui_canvas_emphasize(cid) {
     gui(cid).get_elem("patchsvg", function(e) {
         // raise the window
diff --git a/pd/src/g_canvas.c b/pd/src/g_canvas.c
index b7b6d5ad1431030bd39028c8165d2533f05246d3..95b8b5a7026f4e12dbfe750c784ead48ae884e9b 100644
--- a/pd/src/g_canvas.c
+++ b/pd/src/g_canvas.c
@@ -481,6 +481,8 @@ t_canvas *canvas_new(void *dummy, t_symbol *sel, int argc, t_atom *argv)
     }
     else x->gl_isab = 0;
 
+    x->gl_subdirties = 0;
+
     if (yloc < GLIST_DEFCANVASYLOC)
         yloc = GLIST_DEFCANVASYLOC;
     if (xloc < 0)
@@ -794,6 +796,19 @@ void canvas_dirty(t_canvas *x, t_floatarg n)
         x2->gl_dirty = n;
         if (x2->gl_havewindow)
             canvas_reflecttitle(x2);
+        if (x2->gl_owner)
+        {
+            gobj_isdirty(x2->gl_owner, x2,
+                (x2->gl_dirty ? 1 : (x2->gl_subdirties ? 2 : 0)));
+            x2 = x2->gl_owner;
+            while(x2->gl_owner)
+            {
+                x2->gl_subdirties += (n ? 1 : -1);
+                if(!x2->gl_dirty)
+                    gobj_isdirty(x2->gl_owner, x2, (x2->gl_subdirties ? 2 : 0));
+                x2 = x2->gl_owner;
+            }
+        }
     }
 }
 
diff --git a/pd/src/g_canvas.h b/pd/src/g_canvas.h
index dc4ce7f22903e40411334fdb5d361d891e697751..73078aa5aed45c6fea5118aed61e7feff5a67a47 100644
--- a/pd/src/g_canvas.h
+++ b/pd/src/g_canvas.h
@@ -225,6 +225,7 @@ struct _glist
     unsigned int gl_havewindow:1;   /* true if we own a window */
     unsigned int gl_mapped:1;       /* true if, moreover, it's "mapped" */
     unsigned int gl_dirty:1;        /* (root canvas only:) patch has changed */
+    int gl_subdirties;
     unsigned int gl_loading:1;      /* am now loading from file */
     unsigned int gl_willvis:1;      /* make me visible after loading */ 
     unsigned int gl_edit:1;         /* edit mode */
diff --git a/pd/src/g_editor.c b/pd/src/g_editor.c
index ff61bbd6071238982b8944c4e5c79fa0ca550c97..e526e1fb66307377e48ae204a0307c5c2ee91a7b 100644
--- a/pd/src/g_editor.c
+++ b/pd/src/g_editor.c
@@ -5914,6 +5914,12 @@ static void gobj_emphasize(t_glist *g, t_gobj *x)
     gui_vmess("gui_gobj_emphasize", "xs", g, rtext_gettag(y));
 }
 
+void gobj_isdirty(t_glist *g, t_gobj *x, int on)
+{
+    t_rtext *y = glist_findrtext(g, (t_text *)x);
+    gui_vmess("gui_gobj_setdirty", "xsi", g, rtext_gettag(y), on);
+}
+
 static int glist_dofinderror(t_glist *gl, void *error_object)
 {
     t_gobj *g;
diff --git a/pd/src/g_text.c b/pd/src/g_text.c
index 2a1ef4879702c3c1e9827a7f05ebdf2e8e17111e..b8c25cafd5a108e580ae27df11b8d7679da71c3d 100644
--- a/pd/src/g_text.c
+++ b/pd/src/g_text.c
@@ -2121,6 +2121,14 @@ static void text_vis(t_gobj *z, t_glist *glist, int vis)
                 text_drawborder(x, glist, rtext_gettag(y),
                     rtext_width(y), rtext_height(y), 1);
                 rtext_draw(y);
+
+                if(pd_class(&x->te_pd) == canvas_class)
+                {
+                    if (((t_canvas *)x)->gl_dirty)
+                        gobj_isdirty(glist, x, 1);
+                    else if (((t_canvas *)x)->gl_subdirties)
+                        gobj_isdirty(glist, x, 2);
+                }
             }
         }
         else