Repository: GraphiteEditor/Graphite (language: rust)

Issue:
Refactor artboard text label so it's an overlay
The artboard name drawn above the top left corner of every artboard is currently rendered as part of the artwork. But it isn't part of the artwork, it's an overlay, so the code should be refactored to make it an actual overlay.


Discussion / hints:

Fix patch:
diff --git a/editor/src/consts.rs b/editor/src/consts.rs
index 3dcbd72736..89385ad2f5 100644
--- a/editor/src/consts.rs
+++ b/editor/src/consts.rs
@@ -75,8 +75,10 @@ pub const SCALE_EFFECT: f64 = 0.5;
 // Keep changes to these colors updated with `Editor.svelte`
 pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
 pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848";
-pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
+pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
+pub const COLOR_OVERLAY_RED: &str = "#ef5454";
 pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
+pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
 
 // Document
 pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs
index c393ef6904..cd4cccd3c2 100644
--- a/editor/src/messages/portfolio/document/document_message.rs
+++ b/editor/src/messages/portfolio/document/document_message.rs
@@ -50,6 +50,7 @@ pub enum DocumentMessage {
 	DocumentHistoryBackward,
 	DocumentHistoryForward,
 	DocumentStructureChanged,
+	DrawArtboardOverlays(OverlayContext),
 	DuplicateSelectedLayers,
 	EnterNestedNetwork {
 		node_id: NodeId,
diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs
index 75df1dd54c..9a81703601 100644
--- a/editor/src/messages/portfolio/document/document_message_handler.rs
+++ b/editor/src/messages/portfolio/document/document_message_handler.rs
@@ -1,11 +1,12 @@
 use super::node_graph::utility_types::Transform;
+use super::overlays::utility_types::Pivot;
 use super::utility_types::clipboards::Clipboard;
 use super::utility_types::error::EditorError;
 use super::utility_types::misc::{SnappingOptions, SnappingState, GET_SNAP_BOX_FUNCTIONS, GET_SNAP_GEOMETRY_FUNCTIONS};
 use super::utility_types::network_interface::{NodeNetworkInterface, TransactionStatus};
 use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
 use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH};
-use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
+use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
 use crate::messages::input_mapper::utility_types::macros::action_keys;
 use crate::messages::layout::utility_types::widget_prelude::*;
 use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
@@ -371,6 +372,18 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 				let data_buffer: RawBuffer = self.serialize_root();
 				responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
 			}
+			DocumentMessage::DrawArtboardOverlays(overlay_context) => {
+				for layer in self.metadata().all_layers() {
+					if !self.network_interface.is_artboard(&layer.to_node(), &[]) {
+						continue;
+					}
+					let Some(bounds) = self.metadata().bounding_box_document(layer) else { continue };
+
+					let name = self.network_interface.frontend_display_name(&layer.to_node(), &[]);
+					let transform = self.metadata().document_to_viewport * DAffine2::from_translation(bounds[0].min(bounds[1]) - DVec2::Y * 4.);
+					overlay_context.text_with_transform(&name, COLOR_OVERLAY_GRAY, None, transform, Pivot::BottomLeft);
+				}
+			}
 			DocumentMessage::DuplicateSelectedLayers => {
 				let parent = self.new_layer_parent(false);
 				let calculated_insert_index =
diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs
index 3406457736..7e9c4fafca 100644
--- a/editor/src/messages/portfolio/document/overlays/utility_types.rs
+++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs
@@ -247,47 +247,52 @@ impl OverlayContext {
 		self.render_context.stroke();
 	}
 
-	pub fn text(&self, text: &str, pos: DVec2, background: &str, padding: f64) {
-		let pos = pos.round();
+	pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, pos: DVec2) {
 		let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions");
-		self.render_context.set_fill_style(&background.into());
-		self.render_context.fill_rect(
-			pos.x + metrics.actual_bounding_box_left(),
-			pos.y - metrics.font_bounding_box_ascent() - metrics.font_bounding_box_descent() - padding * 2.,
-			metrics.actual_bounding_box_right() - metrics.actual_bounding_box_left() + padding * 2.,
-			metrics.font_bounding_box_ascent() + metrics.font_bounding_box_descent() + padding * 2.,
-		);
-		self.render_context.set_fill_style(&"white".into());
-		self.render_context
-			.fill_text(text, pos.x + padding, pos.y - padding - metrics.font_bounding_box_descent())
-			.expect("Failed to draw the text on the canvas");
-	}
+		let local_position = pos.round();
 
-	pub fn angle_text(&self, text: &str, pos: DVec2, direction: DVec2, padding: f64, pivot: Pivot) {
-		self.render_context.translate(pos.x, pos.y).expect("Failed to translate the render context to the specified position");
+		self.draw_text(background_color, local_position, metrics, font_color, text);
+	}
 
-		let angle = -direction.angle_to(DVec2::X);
-		self.render_context.rotate(angle).expect("Failed to rotate the render context to the specified angle");
+	pub fn text_with_transform(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, origin: Pivot) {
+		let [a, b, c, d, e, f] = transform.to_cols_array();
+		self.render_context.set_transform(a, b, c, d, e, f).expect("Failed to rotate the render context to the specified angle");
 
 		let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions");
+		let local_position = match origin {
+			Pivot::CenterLeft => DVec2::new(0., (metrics.actual_bounding_box_ascent() + metrics.actual_bounding_box_descent()) / 2.),
+			Pivot::TopCenter => DVec2::new(-(metrics.actual_bounding_box_right() + metrics.actual_bounding_box_left()) / 2., metrics.font_bounding_box_ascent()),
+			Pivot::BottomLeft => DVec2::ZERO,
+		};
 
-		self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
+		self.draw_text(background_color, local_position, metrics, font_color, text);
 
-		let local_position = match pivot {
-			Pivot::LeftCentreY => DVec2::new(padding, (metrics.actual_bounding_box_ascent() + metrics.actual_bounding_box_descent()) / 2.),
-			Pivot::TopCentreX => DVec2::new(
-				-(metrics.actual_bounding_box_right() + metrics.actual_bounding_box_left()) / 2.,
-				padding + metrics.font_bounding_box_ascent(),
-			),
-		};
-		self.render_context
-			.fill_text(text, local_position.x, local_position.y)
-			.expect("Failed to draw the text at the calculated position");
 		self.render_context.reset_transform().expect("Failed to reset the render context transform");
 	}
+
+	fn draw_text(&self, background_color: Option<&str>, local_position: DVec2, metrics: web_sys::TextMetrics, font_color: &str, text: &str) {
+		// Render background
+		if let Some(background) = background_color {
+			self.render_context.set_fill_style(&background.into());
+			self.render_context.fill_rect(
+				local_position.x + metrics.actual_bounding_box_left(),
+				local_position.y - metrics.font_bounding_box_ascent() - metrics.font_bounding_box_descent(),
+				metrics.actual_bounding_box_right() - metrics.actual_bounding_box_left(),
+				metrics.font_bounding_box_ascent() + metrics.font_bounding_box_descent(),
+			);
+		}
+
+		// Render text
+		self.render_context.set_font("12px Source Sans Pro, Arial, sans-serif");
+		self.render_context.set_fill_style(&font_color.into());
+		self.render_context
+			.fill_text(text, local_position.x, local_position.y - metrics.font_bounding_box_descent())
+			.expect("Failed to draw the text on the canvas");
+	}
 }
 
 pub enum Pivot {
-	LeftCentreY,
-	TopCentreX,
+	TopCenter,
+	CenterLeft,
+	BottomLeft,
 }
diff --git a/editor/src/messages/tool/common_functionality/measure.rs b/editor/src/messages/tool/common_functionality/measure.rs
index 24074c79a0..256121d01b 100644
--- a/editor/src/messages/tool/common_functionality/measure.rs
+++ b/editor/src/messages/tool/common_functionality/measure.rs
@@ -1,3 +1,4 @@
+use crate::consts::COLOR_OVERLAY_BLUE;
 use crate::messages::portfolio::document::overlays::utility_types::{self, OverlayContext};
 use crate::messages::tool::tool_messages::tool_prelude::*;
 
@@ -6,7 +7,7 @@ use graphene_std::renderer::Rect;
 pub fn overlay(selected_bounds: Rect, hovered_bounds: Rect, transform: DAffine2, document_to_viewport: DAffine2, overlay_context: &mut OverlayContext) {
 	let transform_to_document = document_to_viewport.inverse() * transform;
 	if selected_bounds.intersects(hovered_bounds) {
-		// TODO: I'm not sure what to do here?
+		// TODO: Figure out what to do here
 		return;
 	}
 
@@ -23,7 +24,8 @@ pub fn overlay(selected_bounds: Rect, hovered_bounds: Rect, transform: DAffine2,
 		overlay_context.line(min_viewport, max_viewport, None);
 		let length = format!("{:.2}", transform_to_document.transform_vector2(DVec2::X * (turn_x - selected_x)).length());
 		let direction = -(min_viewport - max_viewport).normalize_or_zero();
-		overlay_context.angle_text(&length, (min_viewport + max_viewport) / 2., direction, 5., utility_types::Pivot::TopCentreX);
+		let transform = DAffine2::from_translation((min_viewport + max_viewport) / 2.) * DAffine2::from_angle(-direction.angle_to(DVec2::X));
+		overlay_context.text_with_transform(&length, COLOR_OVERLAY_BLUE, None, transform, utility_types::Pivot::TopCenter);
 	}
 	if turn_y != hovered_y {
 		let min_viewport = transform.transform_point2(DVec2::new(turn_x, turn_y.min(hovered_y)));
@@ -31,6 +33,7 @@ pub fn overlay(selected_bounds: Rect, hovered_bounds: Rect, transform: DAffine2,
 		overlay_context.line(min_viewport, max_viewport, None);
 		let length = format!("{:.2}", transform_to_document.transform_vector2(DVec2::Y * (turn_y - hovered_y)).length());
 		let direction = (min_viewport - max_viewport).normalize_or_zero().perp();
-		overlay_context.angle_text(&length, (min_viewport + max_viewport) / 2., direction, 5., utility_types::Pivot::LeftCentreY);
+		let transform = DAffine2::from_translation((min_viewport + max_viewport) / 2.) * DAffine2::from_angle(-direction.angle_to(DVec2::X));
+		overlay_context.text_with_transform(&length, COLOR_OVERLAY_BLUE, None, transform, utility_types::Pivot::CenterLeft);
 	}
 }
diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs
index c20974ddaf..ed0c98b6d9 100644
--- a/editor/src/messages/tool/common_functionality/snapping.rs
+++ b/editor/src/messages/tool/common_functionality/snapping.rs
@@ -5,7 +5,7 @@ mod layer_snapper;
 mod snap_results;
 pub use {alignment_snapper::*, distribution_snapper::*, grid_snapper::*, layer_snapper::*, snap_results::*};
 
-use crate::consts::COLOR_OVERLAY_BLUE;
+use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_WHITE};
 use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
 use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
 use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, GridSnapTarget, SnapTarget};
@@ -466,7 +466,8 @@ impl SnapManager {
 			}
 
 			if !any_align && ind.distribution_equal_distance_x.is_none() && ind.distribution_equal_distance_y.is_none() {
-				overlay_context.text(&format!("{:?} to {:?}", ind.source, ind.target), viewport - DVec2::new(0., 5.), "rgba(0, 0, 0, 0.8)", 3.);
+				let text = format!("{:?} to {:?}", ind.source, ind.target);
+				overlay_context.text(&text, COLOR_OVERLAY_BLUE, Some(COLOR_OVERLAY_WHITE), viewport - DVec2::new(0., 5.));
 				overlay_context.square(viewport, Some(4.), Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE));
 			}
 		}
diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs
index c04ad50f9d..696c6ecbdc 100644
--- a/editor/src/messages/tool/tool_message_handler.rs
+++ b/editor/src/messages/tool/tool_message_handler.rs
@@ -2,6 +2,7 @@ use super::common_functionality::shape_editor::ShapeState;
 use super::utility_types::{tool_message_to_tool_type, ToolActionHandlerData, ToolFsmState};
 use crate::application::generate_uuid;
 use crate::messages::layout::utility_types::widget_prelude::*;
+use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
 use crate::messages::portfolio::utility_types::PersistentData;
 use crate::messages::prelude::*;
 use crate::messages::tool::utility_types::ToolType;
@@ -9,6 +10,8 @@ use crate::node_graph_executor::NodeGraphExecutor;
 
 use graphene_core::raster::color::Color;
 
+const ARTBOARD_OVERLAY_PROVIDER: OverlayProvider = |context| DocumentMessage::DrawArtboardOverlays(context).into();
+
 pub struct ToolMessageData<'a> {
 	pub document_id: DocumentId,
 	pub document: &'a mut DocumentMessageHandler,
@@ -128,6 +131,8 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
 				let message = Box::new(TransformLayerMessage::SelectionChanged.into());
 				let on = BroadcastEvent::SelectionChanged;
 				responses.add(BroadcastMessage::UnsubscribeEvent { message, on });
+
+				responses.add(OverlaysMessage::RemoveProvider(ARTBOARD_OVERLAY_PROVIDER));
 			}
 			ToolMessage::InitTools => {
 				// Subscribe the transform layer to selection change events
@@ -165,6 +170,8 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
 				// Set initial hints and cursor
 				tool_data.active_tool_mut().process_message(ToolMessage::UpdateHints, responses, &mut data);
 				tool_data.active_tool_mut().process_message(ToolMessage::UpdateCursor, responses, &mut data);
+
+				responses.add(OverlaysMessage::AddProvider(ARTBOARD_OVERLAY_PROVIDER));
 			}
 			ToolMessage::PreUndo => {
 				let tool_data = &mut self.tool_state.tool_data;
diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs
index 91b7973b02..2f37e2bf27 100644
--- a/node-graph/gcore/src/graphic_element/renderer.rs
+++ b/node-graph/gcore/src/graphic_element/renderer.rs
@@ -599,22 +599,6 @@ impl GraphicElementRendered for Artboard {
 				attributes.push("height", self.dimensions.y.abs().to_string());
 			});
 		}
-		if !render_params.hide_artboards && !render_params.for_export {
-			// Label
-			render.parent_tag(
-				"text",
-				|attributes| {
-					attributes.push("fill", "white");
-					attributes.push("x", (self.location.x.min(self.location.x + self.dimensions.x)).to_string());
-					attributes.push("y", (self.location.y.min(self.location.y + self.dimensions.y) - 4).to_string());
-					attributes.push("font-size", "14px");
-				},
-				|render| {
-					// TODO: Use the artboard's layer name
-					render.svg.push(self.label.to_string().into());
-				},
-			);
-		}
 
 		// Contents group (includes the artwork but not the background)
 		render.parent_tag(


Test patch: