1using System; 2using System.Drawing; 3using System.Drawing.Drawing2D; 4 5namespace WindowCapture.Models 6{ 7 public enum BubbleShape 8 { 9 RoundedRect, 10 Oval, 11 Rectangle 12 } 13 14 public class CommentBubble 15 { 16 public Point Location; // Top-left of bubble 17 public Point TailPoint; // Where the tail points to 18 public string Text = ""; 19 public Font TextFont; 20 public Color FillColor; 21 public Color BorderColor; 22 public Color TextColor; 23 public int BorderWidth; 24 public bool IsEditing; 25 public int Padding = 10; 26 public int CornerRadius = 8; 27 public int TailWidth = 15; 28 public int TailHeight = 12; 29 public BubbleShape Shape = BubbleShape.RoundedRect; 30 31 public CommentBubble(Point location) 32 { 33 Location = location; 34 TailPoint = new Point(location.X + 20, location.Y + 60); 35 TextFont = new Font(Settings.BubbleFont, Settings.BubbleFontSize); 36 FillColor = Settings.BubbleFillColor; 37 BorderColor = Settings.BubbleBorderColor; 38 TextColor = Color.Black; 39 BorderWidth = Settings.BubbleBorderWidth; 40 IsEditing = true; 41 } 42 43 public Rectangle GetBubbleRect(Graphics g) 44 { 45 SizeF textSize; 46 if (string.IsNullOrEmpty(Text)) 47 textSize = new SizeF(80, 20); // Minimum 48 else 49 textSize = g.MeasureString(Text, TextFont); 50 51 int w = (int)textSize.Width + Padding * 2; 52 int h = (int)textSize.Height + Padding * 2; 53 return new Rectangle(Location.X, Location.Y, w, h); 54 } 55 56 public void CycleShape() 57 { 58 switch (Shape) 59 { 60 case BubbleShape.RoundedRect: 61 Shape = BubbleShape.Oval; 62 break; 63 case BubbleShape.Oval: 64 Shape = BubbleShape.Rectangle; 65 break; 66 case BubbleShape.Rectangle: 67 Shape = BubbleShape.RoundedRect; 68 break; 69 } 70 } 71 72 public GraphicsPath GetBubblePath(Graphics g) 73 { 74 var rect = GetBubbleRect(g); 75 var path = new GraphicsPath(); 76 77 // Determine tail attachment point on bubble edge 78 Point tailAttach = GetTailAttachPoint(rect); 79 80 switch (Shape) 81 { 82 case BubbleShape.Oval: 83 DrawOvalWithTail(path, rect, tailAttach); 84 break; 85 case BubbleShape.Rectangle: 86 DrawRectWithTail(path, rect, tailAttach); 87 break; 88 default: // RoundedRect 89 DrawRoundedRectWithTail(path, rect, tailAttach); 90 break; 91 } 92 93 return path; 94 } 95 96 private Point GetTailAttachPoint(Rectangle rect) 97 { 98 // Calculate which edge the tail should attach to based on TailPoint position 99 int cx = rect.X + rect.Width / 2; 100 int cy = rect.Y + rect.Height / 2; 101 102 // Default: attach to bottom 103 int attachX = Math.Max(rect.X + TailWidth, Math.Min(rect.Right - TailWidth, TailPoint.X)); 104 int attachY = rect.Bottom; 105 106 // Check if tail points to sides or top 107 float dx = TailPoint.X - cx; 108 float dy = TailPoint.Y - cy; 109 110 if (Math.Abs(dx) > Math.Abs(dy)) 111 { 112 // Horizontal - left or right 113 if (dx < 0) 114 { 115 attachX = rect.X; 116 attachY = Math.Max(rect.Y + TailWidth, Math.Min(rect.Bottom - TailWidth, TailPoint.Y)); 117 } 118 else 119 { 120 attachX = rect.Right; 121 attachY = Math.Max(rect.Y + TailWidth, Math.Min(rect.Bottom - TailWidth, TailPoint.Y)); 122 } 123 } 124 else 125 { 126 // Vertical - top or bottom 127 if (dy < 0) 128 { 129 attachY = rect.Y; 130 attachX = Math.Max(rect.X + TailWidth, Math.Min(rect.Right - TailWidth, TailPoint.X)); 131 } 132 else 133 { 134 attachY = rect.Bottom; 135 attachX = Math.Max(rect.X + TailWidth, Math.Min(rect.Right - TailWidth, TailPoint.X)); 136 } 137 } 138 139 return new Point(attachX, attachY); 140 } 141 142 private void DrawRoundedRectWithTail(GraphicsPath path, Rectangle rect, Point tailAttach) 143 { 144 int r = CornerRadius; 145 bool tailOnBottom = tailAttach.Y == rect.Bottom; 146 bool tailOnTop = tailAttach.Y == rect.Y; 147 bool tailOnLeft = tailAttach.X == rect.X; 148 bool tailOnRight = tailAttach.X == rect.Right; 149 150 // Top-left corner 151 path.AddArc(rect.X, rect.Y, r * 2, r * 2, 180, 90); 152 153 // Top edge with tail 154 if (tailOnTop) 155 { 156 path.AddLine(rect.X + r, rect.Y, tailAttach.X - TailWidth / 2, rect.Y); 157 path.AddLine(tailAttach.X - TailWidth / 2, rect.Y, TailPoint.X, TailPoint.Y); 158 path.AddLine(TailPoint.X, TailPoint.Y, tailAttach.X + TailWidth / 2, rect.Y); 159 path.AddLine(tailAttach.X + TailWidth / 2, rect.Y, rect.Right - r, rect.Y); 160 } 161 162 // Top-right corner 163 path.AddArc(rect.Right - r * 2, rect.Y, r * 2, r * 2, 270, 90); 164 165 // Right edge with tail 166 if (tailOnRight) 167 { 168 path.AddLine(rect.Right, rect.Y + r, rect.Right, tailAttach.Y - TailWidth / 2); 169 path.AddLine(rect.Right, tailAttach.Y - TailWidth / 2, TailPoint.X, TailPoint.Y); 170 path.AddLine(TailPoint.X, TailPoint.Y, rect.Right, tailAttach.Y + TailWidth / 2); 171 path.AddLine(rect.Right, tailAttach.Y + TailWidth / 2, rect.Right, rect.Bottom - r); 172 } 173 174 // Bottom-right corner 175 path.AddArc(rect.Right - r * 2, rect.Bottom - r * 2, r * 2, r * 2, 0, 90); 176 177 // Bottom edge with tail 178 if (tailOnBottom) 179 { 180 path.AddLine(rect.Right - r, rect.Bottom, tailAttach.X + TailWidth / 2, rect.Bottom); 181 path.AddLine(tailAttach.X + TailWidth / 2, rect.Bottom, TailPoint.X, TailPoint.Y); 182 path.AddLine(TailPoint.X, TailPoint.Y, tailAttach.X - TailWidth / 2, rect.Bottom); 183 path.AddLine(tailAttach.X - TailWidth / 2, rect.Bottom, rect.X + r, rect.Bottom); 184 } 185 186 // Bottom-left corner 187 path.AddArc(rect.X, rect.Bottom - r * 2, r * 2, r * 2, 90, 90); 188 189 // Left edge with tail 190 if (tailOnLeft) 191 { 192 path.AddLine(rect.X, rect.Bottom - r, rect.X, tailAttach.Y + TailWidth / 2); 193 path.AddLine(rect.X, tailAttach.Y + TailWidth / 2, TailPoint.X, TailPoint.Y); 194 path.AddLine(TailPoint.X, TailPoint.Y, rect.X, tailAttach.Y - TailWidth / 2); 195 path.AddLine(rect.X, tailAttach.Y - TailWidth / 2, rect.X, rect.Y + r); 196 } 197 198 path.CloseFigure(); 199 } 200 201 private void DrawOvalWithTail(GraphicsPath path, Rectangle rect, Point tailAttach) 202 { 203 // Draw ellipse with tail 204 path.AddEllipse(rect); 205 206 // Add tail as separate triangle 207 var tailPath = new GraphicsPath(); 208 int tw = TailWidth / 2; 209 210 // Calculate points on ellipse edge near tail attach 211 float cx = rect.X + rect.Width / 2f; 212 float cy = rect.Y + rect.Height / 2f; 213 float angle = (float)Math.Atan2(TailPoint.Y - cy, TailPoint.X - cx); 214 215 float rx = rect.Width / 2f; 216 float ry = rect.Height / 2f; 217 218 float x1 = cx + rx * (float)Math.Cos(angle - 0.3f); 219 float y1 = cy + ry * (float)Math.Sin(angle - 0.3f); 220 float x2 = cx + rx * (float)Math.Cos(angle + 0.3f); 221 float y2 = cy + ry * (float)Math.Sin(angle + 0.3f); 222 223 path.AddPolygon(new PointF[] { 224 new PointF(x1, y1), 225 new PointF(TailPoint.X, TailPoint.Y), 226 new PointF(x2, y2) 227 }); 228 } 229 230 private void DrawRectWithTail(GraphicsPath path, Rectangle rect, Point tailAttach) 231 { 232 bool tailOnBottom = tailAttach.Y == rect.Bottom; 233 bool tailOnTop = tailAttach.Y == rect.Y; 234 bool tailOnLeft = tailAttach.X == rect.X; 235 bool tailOnRight = tailAttach.X == rect.Right; 236 237 // Start from top-left 238 path.AddLine(rect.X, rect.Y, rect.Right, rect.Y); 239 240 // Top edge with tail 241 if (tailOnTop) 242 { 243 path.Reset(); 244 path.AddLine(rect.X, rect.Y, tailAttach.X - TailWidth / 2, rect.Y); 245 path.AddLine(tailAttach.X - TailWidth / 2, rect.Y, TailPoint.X, TailPoint.Y); 246 path.AddLine(TailPoint.X, TailPoint.Y, tailAttach.X + TailWidth / 2, rect.Y); 247 path.AddLine(tailAttach.X + TailWidth / 2, rect.Y, rect.Right, rect.Y); 248 } 249 250 // Right edge 251 if (tailOnRight) 252 { 253 path.AddLine(rect.Right, rect.Y, rect.Right, tailAttach.Y - TailWidth / 2); 254 path.AddLine(rect.Right, tailAttach.Y - TailWidth / 2, TailPoint.X, TailPoint.Y); 255 path.AddLine(TailPoint.X, TailPoint.Y, rect.Right, tailAttach.Y + TailWidth / 2); 256 path.AddLine(rect.Right, tailAttach.Y + TailWidth / 2, rect.Right, rect.Bottom); 257 } 258 else 259 { 260 path.AddLine(rect.Right, rect.Y, rect.Right, rect.Bottom); 261 } 262 263 // Bottom edge 264 if (tailOnBottom) 265 { 266 path.AddLine(rect.Right, rect.Bottom, tailAttach.X + TailWidth / 2, rect.Bottom); 267 path.AddLine(tailAttach.X + TailWidth / 2, rect.Bottom, TailPoint.X, TailPoint.Y); 268 path.AddLine(TailPoint.X, TailPoint.Y, tailAttach.X - TailWidth / 2, rect.Bottom); 269 path.AddLine(tailAttach.X - TailWidth / 2, rect.Bottom, rect.X, rect.Bottom); 270 } 271 else 272 { 273 path.AddLine(rect.Right, rect.Bottom, rect.X, rect.Bottom); 274 } 275 276 // Left edge 277 if (tailOnLeft) 278 { 279 path.AddLine(rect.X, rect.Bottom, rect.X, tailAttach.Y + TailWidth / 2); 280 path.AddLine(rect.X, tailAttach.Y + TailWidth / 2, TailPoint.X, TailPoint.Y); 281 path.AddLine(TailPoint.X, TailPoint.Y, rect.X, tailAttach.Y - TailWidth / 2); 282 path.AddLine(rect.X, tailAttach.Y - TailWidth / 2, rect.X, rect.Y); 283 } 284 else 285 { 286 path.AddLine(rect.X, rect.Bottom, rect.X, rect.Y); 287 } 288 289 path.CloseFigure(); 290 } 291 292 public bool Contains(Point pt, Graphics g) 293 { 294 using (var path = GetBubblePath(g)) 295 { 296 return path.IsVisible(pt); 297 } 298 } 299 300 public Rectangle GetTailHandleRect() 301 { 302 int handleSize = 8; 303 return new Rectangle(TailPoint.X - handleSize / 2, TailPoint.Y - handleSize / 2, handleSize, handleSize); 304 } 305 306 public Rectangle GetTextBounds(Graphics g) 307 { 308 var bubbleRect = GetBubbleRect(g); 309 return new Rectangle(bubbleRect.X + Padding, bubbleRect.Y + Padding, 310 bubbleRect.Width - Padding * 2, bubbleRect.Height - Padding * 2); 311 } 312 } 313}