@@ -58,6 +58,9 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
5858 /** The size of this comment (which is applied to the editable bubble). */
5959 private bubbleSize = new Size ( DEFAULT_BUBBLE_WIDTH , DEFAULT_BUBBLE_HEIGHT ) ;
6060
61+ /** The location of the comment bubble in workspace coordinates. */
62+ private bubbleLocation ?: Coordinate ;
63+
6164 /**
6265 * The visibility of the bubble for this comment.
6366 *
@@ -149,7 +152,13 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
149152 }
150153
151154 override onLocationChange ( blockOrigin : Coordinate ) : void {
155+ const oldLocation = this . workspaceLocation ;
152156 super . onLocationChange ( blockOrigin ) ;
157+ if ( this . bubbleLocation ) {
158+ const newLocation = this . workspaceLocation ;
159+ const delta = Coordinate . difference ( newLocation , oldLocation ) ;
160+ this . bubbleLocation = Coordinate . sum ( this . bubbleLocation , delta ) ;
161+ }
153162 const anchorLocation = this . getAnchorLocation ( ) ;
154163 this . textInputBubble ?. setAnchorLocation ( anchorLocation ) ;
155164 this . textBubble ?. setAnchorLocation ( anchorLocation ) ;
@@ -191,18 +200,43 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
191200 return this . bubbleSize ;
192201 }
193202
203+ /**
204+ * Sets the location of the comment bubble in the workspace.
205+ */
206+ setBubbleLocation ( location : Coordinate ) {
207+ this . bubbleLocation = location ;
208+ this . textInputBubble ?. moveDuringDrag ( location ) ;
209+ this . textBubble ?. moveDuringDrag ( location ) ;
210+ }
211+
212+ /**
213+ * @returns the location of the comment bubble in the workspace.
214+ */
215+ getBubbleLocation ( ) : Coordinate | undefined {
216+ return this . bubbleLocation ;
217+ }
218+
194219 /**
195220 * @returns the state of the comment as a JSON serializable value if the
196221 * comment has text. Otherwise returns null.
197222 */
198223 saveState ( ) : CommentState | null {
199224 if ( this . text ) {
200- return {
225+ const state : CommentState = {
201226 'text' : this . text ,
202227 'pinned' : this . bubbleIsVisible ( ) ,
203228 'height' : this . bubbleSize . height ,
204229 'width' : this . bubbleSize . width ,
205230 } ;
231+ const location = this . getBubbleLocation ( ) ;
232+ if ( location ) {
233+ state [ 'x' ] = this . sourceBlock . workspace . RTL
234+ ? this . sourceBlock . workspace . getWidth ( ) -
235+ ( location . x + this . bubbleSize . width )
236+ : location . x ;
237+ state [ 'y' ] = location . y ;
238+ }
239+ return state ;
206240 }
207241 return null ;
208242 }
@@ -216,6 +250,16 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
216250 ) ;
217251 this . bubbleVisiblity = state [ 'pinned' ] ?? false ;
218252 this . setBubbleVisible ( this . bubbleVisiblity ) ;
253+ let x = state [ 'x' ] ;
254+ const y = state [ 'y' ] ;
255+ renderManagement . finishQueuedRenders ( ) . then ( ( ) => {
256+ if ( x && y ) {
257+ x = this . sourceBlock . workspace . RTL
258+ ? this . sourceBlock . workspace . getWidth ( ) - ( x + this . bubbleSize . width )
259+ : x ;
260+ this . setBubbleLocation ( new Coordinate ( x , y ) ) ;
261+ }
262+ } ) ;
219263 }
220264
221265 override onClick ( ) : void {
@@ -259,6 +303,12 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
259303 }
260304 }
261305
306+ onBubbleLocationChange ( ) : void {
307+ if ( this . textInputBubble ) {
308+ this . bubbleLocation = this . textInputBubble . getRelativeToSurfaceXY ( ) ;
309+ }
310+ }
311+
262312 bubbleIsVisible ( ) : boolean {
263313 return this . bubbleVisiblity ;
264314 }
@@ -308,8 +358,14 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
308358 ) ;
309359 this . textInputBubble . setText ( this . getText ( ) ) ;
310360 this . textInputBubble . setSize ( this . bubbleSize , true ) ;
361+ if ( this . bubbleLocation ) {
362+ this . textInputBubble . moveDuringDrag ( this . bubbleLocation ) ;
363+ }
311364 this . textInputBubble . addTextChangeListener ( ( ) => this . onTextChange ( ) ) ;
312365 this . textInputBubble . addSizeChangeListener ( ( ) => this . onSizeChange ( ) ) ;
366+ this . textInputBubble . addLocationChangeListener ( ( ) =>
367+ this . onBubbleLocationChange ( ) ,
368+ ) ;
313369 }
314370
315371 /** Shows the non editable text bubble for this comment. */
@@ -320,6 +376,9 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
320376 this . getAnchorLocation ( ) ,
321377 this . getBubbleOwnerRect ( ) ,
322378 ) ;
379+ if ( this . bubbleLocation ) {
380+ this . textBubble . moveDuringDrag ( this . bubbleLocation ) ;
381+ }
323382 }
324383
325384 /** Hides any open bubbles owned by this comment. */
@@ -365,6 +424,12 @@ export interface CommentState {
365424
366425 /** The width of the comment bubble. */
367426 width ?: number ;
427+
428+ /** The X coordinate of the comment bubble. */
429+ x ?: number ;
430+
431+ /** The Y coordinate of the comment bubble. */
432+ y ?: number ;
368433}
369434
370435registry . register ( CommentIcon . TYPE , CommentIcon ) ;
0 commit comments