1use std::cell::Cell;
27use std::rc::Rc;
28use std::sync::Arc;
29
30use crate::color::Color;
31use crate::draw_ctx::DrawCtx;
32use crate::event::{Event, EventResult, MouseButton};
33use crate::geometry::{Point, Rect, Size};
34use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
35use crate::text::Font;
36use crate::widget::{Widget, paint_subtree};
37use crate::widgets::button::Button;
38use crate::widgets::checkbox::Checkbox;
39
40const SWATCH_H: f64 = 22.0;
43const SWATCH_MIN_W:f64 = 48.0;
44
45const PANEL_W: f64 = 228.0;
46const PAD: f64 = 8.0;
47const ROW_GAP: f64 = 6.0;
48
49const HUE_H: f64 = 16.0;
50const SV_H: f64 = 140.0;
51const ALPHA_H: f64 = 16.0;
52const HEX_H: f64 = 20.0;
53const CHECK_H: f64 = 20.0;
54const BTN_H: f64 = 26.0;
55
56fn panel_body_h(allow_none: bool) -> f64 {
58 let mut h = PAD;
59 h += HUE_H + ROW_GAP;
60 h += SV_H + ROW_GAP;
61 h += ALPHA_H + ROW_GAP;
62 h += HEX_H + ROW_GAP;
63 if allow_none { h += CHECK_H + ROW_GAP; }
64 h += BTN_H + PAD;
65 h
66}
67
68fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
71 let max = r.max(g).max(b);
72 let min = r.min(g).min(b);
73 let d = max - min;
74 let v = max;
75 let s = if max <= 0.0 { 0.0 } else { d / max };
76 let h = if d <= 0.0 {
77 0.0
78 } else if max == r {
79 60.0 * (((g - b) / d) % 6.0)
80 } else if max == g {
81 60.0 * (((b - r) / d) + 2.0)
82 } else {
83 60.0 * (((r - g) / d) + 4.0)
84 };
85 let h = if h < 0.0 { h + 360.0 } else { h };
86 (h / 360.0, s, v)
87}
88
89fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
90 let h6 = (h * 6.0) % 6.0;
91 let c = v * s;
92 let x = c * (1.0 - (h6 % 2.0 - 1.0).abs());
93 let (r1, g1, b1) = match h6 as i32 {
94 0 => (c, x, 0.0),
95 1 => (x, c, 0.0),
96 2 => (0.0, c, x),
97 3 => (0.0, x, c),
98 4 => (x, 0.0, c),
99 _ => (c, 0.0, x),
100 };
101 let m = v - c;
102 (r1 + m, g1 + m, b1 + m)
103}
104
105fn format_hex(c: Color) -> String {
106 let r = (c.r * 255.0).clamp(0.0, 255.0) as u32;
107 let g = (c.g * 255.0).clamp(0.0, 255.0) as u32;
108 let b = (c.b * 255.0).clamp(0.0, 255.0) as u32;
109 let a = (c.a * 255.0).clamp(0.0, 255.0) as u32;
110 format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
111}
112
113#[derive(Clone, Copy, Debug, PartialEq)]
116enum Drag { None, Hue, Sv, Alpha }
117
118pub struct ColorPicker {
122 bounds: Rect,
123 children: Vec<Box<dyn Widget>>, base: WidgetBase,
125
126 font: Arc<Font>,
127 font_size: f64,
128
129 color_cell: Rc<Cell<Color>>,
132
133 saved: Color,
135
136 open: bool,
138 h: f32, s: f32, v: f32, a: f32,
139 no_color: bool,
142 allow_none: bool,
143
144 drag: Drag,
146
147 hovered: bool,
149
150 on_select: Option<Box<dyn FnMut(Color)>>,
152
153 idx_cancel: usize,
156 idx_select: usize,
157 idx_none: Option<usize>,
158
159 none_cell: Rc<Cell<bool>>,
162 cancel_flag: Rc<Cell<bool>>,
164 select_flag: Rc<Cell<bool>>,
165}
166
167impl ColorPicker {
168 pub fn new(color_cell: Rc<Cell<Color>>, font: Arc<Font>) -> Self {
169 let initial = color_cell.get();
170 let (h, s, v) = rgb_to_hsv(initial.r, initial.g, initial.b);
171 let none_cell = Rc::new(Cell::new(false));
172 let cancel_flag = Rc::new(Cell::new(false));
173 let select_flag = Rc::new(Cell::new(false));
174
175 let mut me = Self {
176 bounds: Rect::default(),
177 children: Vec::new(),
178 base: WidgetBase::new(),
179 font: Arc::clone(&font),
180 font_size: 13.0,
181 color_cell,
182 saved: initial,
183 open: false,
184 h, s, v,
185 a: initial.a,
186 no_color: initial.a <= 0.0,
187 allow_none: false,
188 drag: Drag::None,
189 hovered: false,
190 on_select: None,
191 idx_cancel: 0,
192 idx_select: 1,
193 idx_none: None,
194 none_cell,
195 cancel_flag,
196 select_flag,
197 };
198 me.build_children();
199 me
200 }
201
202 pub fn with_font_size(mut self, s: f64) -> Self { self.font_size = s; self }
203 pub fn with_allow_none(mut self, allow: bool) -> Self {
204 self.allow_none = allow;
205 self.build_children();
206 self
207 }
208 pub fn on_select(mut self, cb: impl FnMut(Color) + 'static) -> Self {
209 self.on_select = Some(Box::new(cb));
210 self
211 }
212
213 pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
214 pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
215 pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
216 pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
217 pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
218
219 fn build_children(&mut self) {
220 self.children.clear();
221
222 let cf = Rc::clone(&self.cancel_flag);
223 let sf = Rc::clone(&self.select_flag);
224
225 let cancel = Button::new("Cancel", Arc::clone(&self.font))
226 .on_click(move || cf.set(true));
227 let select = Button::new("Select", Arc::clone(&self.font))
228 .on_click(move || sf.set(true));
229
230 if self.allow_none {
231 let none_check = Checkbox::new(
232 "No Color (Pass Through)",
233 Arc::clone(&self.font),
234 self.no_color,
235 )
236 .with_font_size(self.font_size)
237 .with_state_cell(Rc::clone(&self.none_cell));
238 self.children.push(Box::new(none_check));
239 self.idx_none = Some(0);
240 self.idx_cancel = 1;
241 self.idx_select = 2;
242 } else {
243 self.idx_none = None;
244 self.idx_cancel = 0;
245 self.idx_select = 1;
246 }
247 self.children.push(Box::new(cancel));
248 self.children.push(Box::new(select));
249 }
250
251 fn sync_color_from_hsva(&self) -> Color {
252 if self.no_color {
253 Color::transparent()
254 } else {
255 let (r, g, b) = hsv_to_rgb(self.h, self.s, self.v);
256 Color::rgba(r, g, b, self.a)
257 }
258 }
259
260 fn commit(&mut self) {
261 let c = self.sync_color_from_hsva();
262 self.color_cell.set(c);
263 if let Some(cb) = self.on_select.as_mut() { cb(c); }
264 self.open = false;
265 }
266
267 fn cancel(&mut self) {
268 self.color_cell.set(self.saved);
269 let (h, s, v) = rgb_to_hsv(self.saved.r, self.saved.g, self.saved.b);
270 self.h = h; self.s = s; self.v = v;
271 self.a = self.saved.a;
272 self.no_color = self.saved.a <= 0.0;
273 self.none_cell.set(self.no_color);
274 self.open = false;
275 }
276
277 fn regions(&self) -> PanelRegions {
281 let w = self.bounds.width;
282 let h = self.bounds.height;
283
284 let swatch = Rect::new(0.0, h - SWATCH_H, w, SWATCH_H);
285
286 let mut y = h - SWATCH_H - PAD;
288
289 y -= HUE_H;
290 let hue = Rect::new(PAD, y, w - PAD * 2.0, HUE_H);
291 y -= ROW_GAP;
292
293 y -= SV_H;
294 let sv = Rect::new(PAD, y, w - PAD * 2.0, SV_H);
295 y -= ROW_GAP;
296
297 y -= ALPHA_H;
298 let alpha = Rect::new(PAD, y, w - PAD * 2.0, ALPHA_H);
299 y -= ROW_GAP;
300
301 y -= HEX_H;
302 let hex = Rect::new(PAD, y, w - PAD * 2.0, HEX_H);
303 y -= ROW_GAP;
304
305 let none = if self.allow_none {
306 y -= CHECK_H;
307 let r = Rect::new(PAD, y, w - PAD * 2.0, CHECK_H);
308 Some(r)
309 } else { None };
310 let _ = y;
311
312 let btns_y = PAD;
313 let btn_w = (w - PAD * 3.0) * 0.5;
314 let cancel = Rect::new(PAD, btns_y, btn_w, BTN_H);
315 let select = Rect::new(PAD + btn_w + PAD, btns_y, btn_w, BTN_H);
316
317 PanelRegions { swatch, hue, sv, alpha, hex, none, cancel, select }
318 }
319}
320
321struct PanelRegions {
322 swatch: Rect,
323 hue: Rect,
324 sv: Rect,
325 alpha: Rect,
326 hex: Rect,
327 none: Option<Rect>,
328 cancel: Rect,
329 select: Rect,
330}
331
332impl Widget for ColorPicker {
333 fn type_name(&self) -> &'static str { "ColorPicker" }
334 fn bounds(&self) -> Rect { self.bounds }
335 fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
336 fn children(&self) -> &[Box<dyn Widget>] { &self.children }
337 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
338
339 fn margin(&self) -> Insets { self.base.margin }
340 fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
341 fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
342 fn min_size(&self) -> Size { self.base.min_size }
343 fn max_size(&self) -> Size { self.base.max_size }
344
345 fn layout(&mut self, available: Size) -> Size {
346 self.no_color = self.none_cell.get();
348
349 let w = if self.open {
350 PANEL_W.min(available.width.max(PANEL_W))
351 } else {
352 available.width.max(SWATCH_MIN_W).min(PANEL_W)
353 };
354
355 let h = if self.open {
356 SWATCH_H + panel_body_h(self.allow_none)
357 } else {
358 SWATCH_H
359 };
360
361 self.bounds = Rect::new(0.0, 0.0, w, h);
362
363 if self.open {
364 let r = self.regions();
365 if let Some(none_rect) = r.none {
367 if let Some(idx) = self.idx_none {
368 let cb = &mut self.children[idx];
369 cb.layout(Size::new(none_rect.width, none_rect.height));
370 cb.set_bounds(none_rect);
371 }
372 }
373 let cb = &mut self.children[self.idx_cancel];
374 cb.layout(Size::new(r.cancel.width, r.cancel.height));
375 cb.set_bounds(r.cancel);
376
377 let sb = &mut self.children[self.idx_select];
378 sb.layout(Size::new(r.select.width, r.select.height));
379 sb.set_bounds(r.select);
380 } else {
381 for c in self.children.iter_mut() {
383 c.set_bounds(Rect::new(0.0, 0.0, 0.0, 0.0));
384 }
385 }
386
387 Size::new(w, h)
388 }
389
390 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
391 let v = ctx.visuals();
392 let r = self.regions();
393
394 if self.open {
396 ctx.set_fill_color(v.widget_bg);
397 ctx.begin_path();
398 ctx.rounded_rect(0.0, 0.0, self.bounds.width, self.bounds.height, 6.0);
399 ctx.fill();
400 ctx.set_stroke_color(v.widget_stroke);
401 ctx.set_line_width(1.0);
402 ctx.begin_path();
403 ctx.rounded_rect(0.0, 0.0, self.bounds.width, self.bounds.height, 6.0);
404 ctx.stroke();
405 }
406
407 paint_checker_bg(ctx, r.swatch, 6.0);
409 let cur = self.sync_color_from_hsva();
410 ctx.set_fill_color(cur);
411 ctx.begin_path();
412 ctx.rounded_rect(r.swatch.x, r.swatch.y, r.swatch.width, r.swatch.height, 4.0);
413 ctx.fill();
414 ctx.set_stroke_color(v.widget_stroke);
415 ctx.set_line_width(1.0);
416 ctx.begin_path();
417 ctx.rounded_rect(r.swatch.x, r.swatch.y, r.swatch.width, r.swatch.height, 4.0);
418 ctx.stroke();
419
420 if !self.open { return; }
421
422 paint_hue_strip(ctx, r.hue);
424 paint_vertical_marker(ctx, r.hue, self.h, v.widget_stroke_active);
425
426 paint_sv_rect(ctx, r.sv, self.h);
428 let mx = r.sv.x + self.s as f64 * r.sv.width;
429 let my = r.sv.y + self.v as f64 * r.sv.height;
431 paint_crosshair(ctx, mx, my, v.widget_stroke_active);
432
433 paint_checker_bg(ctx, r.alpha, 4.0);
435 paint_alpha_strip(ctx, r.alpha, cur);
436 paint_vertical_marker(ctx, r.alpha, self.a, v.widget_stroke_active);
437
438 ctx.set_fill_color(v.widget_bg_hovered);
440 ctx.begin_path();
441 ctx.rounded_rect(r.hex.x, r.hex.y, r.hex.width, r.hex.height, 3.0);
442 ctx.fill();
443 ctx.set_stroke_color(v.widget_stroke);
444 ctx.set_line_width(1.0);
445 ctx.begin_path();
446 ctx.rounded_rect(r.hex.x, r.hex.y, r.hex.width, r.hex.height, 3.0);
447 ctx.stroke();
448
449 let hex = format_hex(cur);
450 ctx.set_font(Arc::clone(&self.font));
451 ctx.set_font_size(self.font_size);
452 ctx.set_fill_color(v.text_color);
453 let text_w = ctx.measure_text(&hex).map(|m| m.width).unwrap_or(0.0);
454 let tx = r.hex.x + (r.hex.width - text_w) * 0.5;
455 let ty = r.hex.y + (r.hex.height - self.font_size) * 0.5 + 2.0;
456 ctx.fill_text(&hex, tx, ty);
457
458 for child in self.children.iter_mut() {
460 let b = child.bounds();
461 if b.width <= 0.0 || b.height <= 0.0 { continue; }
462 ctx.save();
463 ctx.translate(b.x, b.y);
464 paint_subtree(child.as_mut(), ctx);
465 ctx.restore();
466 }
467 }
468
469 fn on_event(&mut self, event: &Event) -> EventResult {
470 if self.open {
477 let local_pt = match event {
478 Event::MouseMove { pos } => Some(*pos),
479 Event::MouseDown { pos, .. } => Some(*pos),
480 Event::MouseUp { pos, .. } => Some(*pos),
481 _ => None,
482 };
483
484 for child in self.children.iter_mut() {
485 let b = child.bounds();
486 if b.width <= 0.0 || b.height <= 0.0 { continue; }
487 if let Some(p) = local_pt {
491 if !contains(&b, p) { continue; }
492 let lp = Point::new(p.x - b.x, p.y - b.y);
493 let translated = translate_mouse_event(event, lp);
494 let res = child.on_event(&translated);
495 if res == EventResult::Consumed {
496 self.handle_btn_flags();
497 return EventResult::Consumed;
498 }
499 } else {
500 let res = child.on_event(event);
501 if res == EventResult::Consumed {
502 self.handle_btn_flags();
503 return EventResult::Consumed;
504 }
505 }
506 }
507 self.handle_btn_flags();
508 }
509
510 match event {
511 Event::MouseDown { button: MouseButton::Left, pos, .. } => {
512 let r = self.regions();
513 if !self.open {
514 if contains(&r.swatch, *pos) {
515 self.open = true;
517 self.saved = self.color_cell.get();
518 let (h, s, v) = rgb_to_hsv(self.saved.r, self.saved.g, self.saved.b);
519 self.h = h; self.s = s; self.v = v; self.a = self.saved.a;
520 self.no_color = self.saved.a <= 0.0;
521 self.none_cell.set(self.no_color);
522 crate::animation::request_tick();
523 return EventResult::Consumed;
524 }
525 return EventResult::Ignored;
526 }
527 if contains(&r.hue, *pos) {
528 self.drag = Drag::Hue;
529 self.h = ((pos.x - r.hue.x) / r.hue.width).clamp(0.0, 1.0) as f32;
530 crate::animation::request_tick();
531 return EventResult::Consumed;
532 }
533 if contains(&r.sv, *pos) {
534 self.drag = Drag::Sv;
535 self.s = ((pos.x - r.sv.x) / r.sv.width).clamp(0.0, 1.0) as f32;
536 self.v = ((pos.y - r.sv.y) / r.sv.height).clamp(0.0, 1.0) as f32;
537 crate::animation::request_tick();
538 return EventResult::Consumed;
539 }
540 if contains(&r.alpha, *pos) {
541 self.drag = Drag::Alpha;
542 self.a = ((pos.x - r.alpha.x) / r.alpha.width).clamp(0.0, 1.0) as f32;
543 crate::animation::request_tick();
544 return EventResult::Consumed;
545 }
546 EventResult::Ignored
547 }
548 Event::MouseMove { pos } => {
549 self.hovered = self.hit_test(*pos);
550 if self.drag == Drag::None { return EventResult::Ignored; }
551 let r = self.regions();
552 match self.drag {
553 Drag::Hue => {
554 self.h = ((pos.x - r.hue.x) / r.hue.width).clamp(0.0, 1.0) as f32;
555 }
556 Drag::Sv => {
557 self.s = ((pos.x - r.sv.x) / r.sv.width).clamp(0.0, 1.0) as f32;
558 self.v = ((pos.y - r.sv.y) / r.sv.height).clamp(0.0, 1.0) as f32;
559 }
560 Drag::Alpha => {
561 self.a = ((pos.x - r.alpha.x) / r.alpha.width).clamp(0.0, 1.0) as f32;
562 }
563 Drag::None => {}
564 }
565 if !self.no_color {
568 let c = self.sync_color_from_hsva();
569 self.color_cell.set(c);
570 }
571 crate::animation::request_tick();
572 EventResult::Consumed
573 }
574 Event::MouseUp { button: MouseButton::Left, .. } => {
575 let was_dragging = self.drag != Drag::None;
576 self.drag = Drag::None;
577 if was_dragging { EventResult::Consumed } else { EventResult::Ignored }
578 }
579 _ => EventResult::Ignored,
580 }
581 }
582}
583
584impl ColorPicker {
585 fn handle_btn_flags(&mut self) {
586 if self.cancel_flag.get() {
587 self.cancel_flag.set(false);
588 self.cancel();
589 }
590 if self.select_flag.get() {
591 self.select_flag.set(false);
592 self.commit();
593 }
594 if self.open {
595 let want = self.none_cell.get();
596 if want != self.no_color {
597 self.no_color = want;
598 if want {
599 self.color_cell.set(Color::transparent());
601 } else {
602 let c = self.sync_color_from_hsva();
603 self.color_cell.set(c);
604 }
605 crate::animation::request_tick();
606 }
607 }
608 }
609}
610
611fn contains(r: &Rect, p: Point) -> bool {
614 p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height
615}
616
617fn translate_mouse_event(e: &Event, p: Point) -> Event {
618 match e {
619 Event::MouseMove { .. } => Event::MouseMove { pos: p },
620 Event::MouseDown { button, modifiers, .. } =>
621 Event::MouseDown { button: *button, pos: p, modifiers: *modifiers },
622 Event::MouseUp { button, modifiers, .. } =>
623 Event::MouseUp { button: *button, pos: p, modifiers: *modifiers },
624 _ => e.clone(),
625 }
626}
627
628fn paint_checker_bg(ctx: &mut dyn DrawCtx, r: Rect, tile: f64) {
629 ctx.set_fill_color(Color::rgb(0.75, 0.75, 0.75));
630 ctx.begin_path();
631 ctx.rect(r.x, r.y, r.width, r.height);
632 ctx.fill();
633
634 ctx.set_fill_color(Color::rgb(0.45, 0.45, 0.45));
635 let cols = (r.width / tile).ceil() as i32;
636 let rows = (r.height / tile).ceil() as i32;
637 for row in 0..rows {
638 for col in 0..cols {
639 if (row + col) & 1 == 0 {
640 let x = r.x + col as f64 * tile;
641 let y = r.y + row as f64 * tile;
642 let w = (tile).min(r.x + r.width - x).max(0.0);
643 let h = (tile).min(r.y + r.height - y).max(0.0);
644 if w > 0.0 && h > 0.0 {
645 ctx.begin_path();
646 ctx.rect(x, y, w, h);
647 ctx.fill();
648 }
649 }
650 }
651 }
652}
653
654fn paint_hue_strip(ctx: &mut dyn DrawCtx, r: Rect) {
655 let steps = r.width.ceil() as i32;
656 let step_w = r.width / steps as f64;
657 for i in 0..steps {
658 let t = i as f32 / steps as f32;
659 let (cr, cg, cb) = hsv_to_rgb(t, 1.0, 1.0);
660 ctx.set_fill_color(Color::rgb(cr, cg, cb));
661 ctx.begin_path();
662 ctx.rect(r.x + i as f64 * step_w, r.y, step_w + 1.0, r.height);
663 ctx.fill();
664 }
665}
666
667fn paint_sv_rect(ctx: &mut dyn DrawCtx, r: Rect, hue: f32) {
668 let (hr, hg, hb) = hsv_to_rgb(hue, 1.0, 1.0);
677
678 let cols = r.width.ceil() as i32;
679 let col_w = r.width / cols as f64;
680 for i in 0..cols {
681 let tx = i as f32 / cols as f32;
682 let cr = 1.0 * (1.0 - tx) + hr * tx;
684 let cg = 1.0 * (1.0 - tx) + hg * tx;
685 let cb = 1.0 * (1.0 - tx) + hb * tx;
686 ctx.set_fill_color(Color::rgb(cr, cg, cb));
687 ctx.begin_path();
688 ctx.rect(r.x + i as f64 * col_w, r.y, col_w + 1.0, r.height);
689 ctx.fill();
690 }
691
692 let rows = r.height.ceil() as i32;
694 let row_h = r.height / rows as f64;
695 for j in 0..rows {
696 let ty = j as f32 / rows as f32; let alpha = 1.0 - ty;
699 ctx.set_fill_color(Color::rgba(0.0, 0.0, 0.0, alpha));
700 ctx.begin_path();
701 ctx.rect(r.x, r.y + j as f64 * row_h, r.width, row_h + 1.0);
702 ctx.fill();
703 }
704}
705
706fn paint_alpha_strip(ctx: &mut dyn DrawCtx, r: Rect, c: Color) {
707 let steps = r.width.ceil() as i32;
708 let step_w = r.width / steps as f64;
709 for i in 0..steps {
710 let t = i as f32 / steps as f32;
711 ctx.set_fill_color(Color::rgba(c.r, c.g, c.b, t));
712 ctx.begin_path();
713 ctx.rect(r.x + i as f64 * step_w, r.y, step_w + 1.0, r.height);
714 ctx.fill();
715 }
716}
717
718fn paint_vertical_marker(ctx: &mut dyn DrawCtx, r: Rect, t: f32, col: Color) {
719 let x = r.x + (t.clamp(0.0, 1.0) as f64) * r.width;
720 ctx.set_stroke_color(Color::white());
721 ctx.set_line_width(3.0);
722 ctx.begin_path();
723 ctx.move_to(x, r.y);
724 ctx.line_to(x, r.y + r.height);
725 ctx.stroke();
726 ctx.set_stroke_color(col);
727 ctx.set_line_width(1.5);
728 ctx.begin_path();
729 ctx.move_to(x, r.y);
730 ctx.line_to(x, r.y + r.height);
731 ctx.stroke();
732}
733
734fn paint_crosshair(ctx: &mut dyn DrawCtx, x: f64, y: f64, col: Color) {
735 ctx.set_stroke_color(Color::white());
736 ctx.set_line_width(3.0);
737 ctx.begin_path();
738 ctx.circle(x, y, 5.0);
739 ctx.stroke();
740 ctx.set_stroke_color(col);
741 ctx.set_line_width(1.5);
742 ctx.begin_path();
743 ctx.circle(x, y, 5.0);
744 ctx.stroke();
745}