| 1 | use std::{ |
| 2 | cell::{RefCell, RefMut}, |
| 3 | f32::consts::PI, |
| 4 | slice, |
| 5 | }; |
| 6 | |
| 7 | use crate::geometry::{Position, Transform2D, Vector}; |
| 8 | use rustybuzz::ttf_parser; |
| 9 | |
| 10 | mod cache; |
| 11 | pub use cache::{Convexity, PathCache}; |
| 12 | |
| 13 | // Length proportional to radius of a cubic bezier handle for 90deg arcs. |
| 14 | const KAPPA90: f32 = 0.552_284_8; // 0.552_284_749_3; |
| 15 | |
| 16 | /// Specifies whether a shape is solid or a hole when adding it to a path. |
| 17 | /// |
| 18 | /// The default value is `Solid`. |
| 19 | #[derive (Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Default)] |
| 20 | pub enum Solidity { |
| 21 | /// The shape is solid (filled). |
| 22 | #[default] |
| 23 | Solid = 1, |
| 24 | /// The shape is a hole (not filled). |
| 25 | Hole = 2, |
| 26 | } |
| 27 | |
| 28 | #[derive (Copy, Clone, Debug, Eq, PartialEq)] |
| 29 | #[repr (u8)] |
| 30 | #[cfg_attr (feature = "serde" , derive(Serialize, Deserialize))] |
| 31 | pub enum PackedVerb { |
| 32 | MoveTo, |
| 33 | LineTo, |
| 34 | BezierTo, |
| 35 | Solid, |
| 36 | Hole, |
| 37 | Close, |
| 38 | } |
| 39 | |
| 40 | /// A verb describes how to interpret one or more points to continue the countour |
| 41 | /// of a [`Path`]. |
| 42 | #[derive (Copy, Clone, Debug)] |
| 43 | pub enum Verb { |
| 44 | /// Terminates the current sub-path and defines the new current point by the |
| 45 | /// given x/y f32 coordinates. |
| 46 | MoveTo(f32, f32), |
| 47 | /// Describes that the contour of the path should continue as a line from the |
| 48 | /// current point to the given x/y f32 coordinates. |
| 49 | LineTo(f32, f32), |
| 50 | /// Describes that the contour of the path should continue as a cubie bezier segment from the |
| 51 | /// current point via two control points (as f32 pairs) to the point in the last f32 pair. |
| 52 | BezierTo(f32, f32, f32, f32, f32, f32), |
| 53 | /// Sets the current sub-path winding to be solid. |
| 54 | Solid, |
| 55 | /// Sets the current sub-path winding to be hole. |
| 56 | Hole, |
| 57 | /// Closes the current sub-path. |
| 58 | Close, |
| 59 | } |
| 60 | |
| 61 | impl Verb { |
| 62 | fn num_coordinates(&self) -> usize { |
| 63 | match *self { |
| 64 | Self::MoveTo(..) => 1, |
| 65 | Self::LineTo(..) => 1, |
| 66 | Self::BezierTo(..) => 3, |
| 67 | Self::Solid => 0, |
| 68 | Self::Hole => 0, |
| 69 | Self::Close => 0, |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | fn from_packed(packed: &PackedVerb, coords: &[Position]) -> Self { |
| 74 | match *packed { |
| 75 | PackedVerb::MoveTo => Self::MoveTo(coords[0].x, coords[0].y), |
| 76 | PackedVerb::LineTo => Self::LineTo(coords[0].x, coords[0].y), |
| 77 | PackedVerb::BezierTo => Self::BezierTo( |
| 78 | coords[0].x, |
| 79 | coords[0].y, |
| 80 | coords[1].x, |
| 81 | coords[1].y, |
| 82 | coords[2].x, |
| 83 | coords[2].y, |
| 84 | ), |
| 85 | PackedVerb::Solid => Self::Solid, |
| 86 | PackedVerb::Hole => Self::Hole, |
| 87 | PackedVerb::Close => Self::Close, |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | /// A collection of verbs (`move_to()`, `line_to()`, `bezier_to()`, etc.) |
| 93 | /// describing one or more contours. |
| 94 | #[derive (Default, Clone, Debug)] |
| 95 | #[cfg_attr (feature = "serde" , derive(Serialize, Deserialize))] |
| 96 | pub struct Path { |
| 97 | verbs: Vec<PackedVerb>, |
| 98 | coords: Vec<Position>, |
| 99 | last_pos: Position, |
| 100 | dist_tol: f32, |
| 101 | #[cfg_attr (feature = "serde" , serde(skip))] |
| 102 | pub(crate) cache: RefCell<Option<(u64, PathCache)>>, |
| 103 | } |
| 104 | |
| 105 | impl Path { |
| 106 | /// Creates a new empty path with a distance tolerance of 0.01. |
| 107 | pub fn new() -> Self { |
| 108 | Self { |
| 109 | dist_tol: 0.01, |
| 110 | ..Default::default() |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | /// Returns the memory size in bytes used by the path. |
| 115 | pub fn size(&self) -> usize { |
| 116 | std::mem::size_of::<PackedVerb>() * self.verbs.len() + std::mem::size_of::<f32>() * self.coords.len() |
| 117 | } |
| 118 | |
| 119 | /// Checks if the path is empty (contains no verbs). |
| 120 | pub fn is_empty(&self) -> bool { |
| 121 | self.verbs.is_empty() |
| 122 | } |
| 123 | |
| 124 | /// Sets the distance tolerance used for path operations. |
| 125 | pub fn set_distance_tolerance(&mut self, value: f32) { |
| 126 | self.dist_tol = value; |
| 127 | } |
| 128 | |
| 129 | /// Returns an iterator over the path's verbs. |
| 130 | pub fn verbs(&self) -> PathIter<'_> { |
| 131 | PathIter { |
| 132 | verbs: self.verbs.iter(), |
| 133 | coords: &self.coords, |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | pub(crate) fn cache<'a>(&'a self, transform: &Transform2D, tess_tol: f32, dist_tol: f32) -> RefMut<'a, PathCache> { |
| 138 | // The path cache saves a flattened and transformed version of the path. If client code calls |
| 139 | // (fill|stroke)_path repeatedly with the same Path under the same transform circumstances then it will be |
| 140 | // retrieved from cache. I'm not sure if transform.cache_key() is actually good enough for this |
| 141 | // and if it will produce the correct cache keys under different float edge cases. |
| 142 | |
| 143 | let key = transform.cache_key(); |
| 144 | |
| 145 | // this shouldn't need a bool once non lexic lifetimes are stable |
| 146 | let mut needs_rebuild = true; |
| 147 | |
| 148 | if let Some((transform_cache_key, _cache)) = &*self.cache.borrow() { |
| 149 | needs_rebuild = key != *transform_cache_key; |
| 150 | } |
| 151 | |
| 152 | if needs_rebuild { |
| 153 | let path_cache = PathCache::new(self.verbs(), transform, tess_tol, dist_tol); |
| 154 | *self.cache.borrow_mut() = Some((key, path_cache)); |
| 155 | } |
| 156 | |
| 157 | RefMut::map(self.cache.borrow_mut(), |cache| &mut cache.as_mut().unwrap().1) |
| 158 | } |
| 159 | |
| 160 | // Path funcs |
| 161 | |
| 162 | /// Starts a new sub-path with the specified point as the first point. |
| 163 | pub fn move_to(&mut self, x: f32, y: f32) { |
| 164 | self.append(&[PackedVerb::MoveTo], &[Position { x, y }]); |
| 165 | } |
| 166 | |
| 167 | /// Adds a line segment from the last point in the path to the specified point. |
| 168 | pub fn line_to(&mut self, x: f32, y: f32) { |
| 169 | self.append(&[PackedVerb::LineTo], &[Position { x, y }]); |
| 170 | } |
| 171 | |
| 172 | /// Adds a cubic bezier segment from the last point in the path via two control points to the specified point. |
| 173 | pub fn bezier_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) { |
| 174 | self.append( |
| 175 | &[PackedVerb::BezierTo], |
| 176 | &[ |
| 177 | Position { x: c1x, y: c1y }, |
| 178 | Position { x: c2x, y: c2y }, |
| 179 | Position { x, y }, |
| 180 | ], |
| 181 | ); |
| 182 | } |
| 183 | |
| 184 | /// Adds a quadratic bezier segment from the last point in the path via a control point to the specified point. |
| 185 | pub fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { |
| 186 | let pos0 = self.last_pos; |
| 187 | let cpos = Position { x: cx, y: cy }; |
| 188 | let pos = Position { x, y }; |
| 189 | let pos1 = pos0 + (cpos - pos0) * (2.0 / 3.0); |
| 190 | let pos2 = pos + (cpos - pos) * (2.0 / 3.0); |
| 191 | |
| 192 | self.append(&[PackedVerb::BezierTo], &[pos1, pos2, pos]); |
| 193 | } |
| 194 | |
| 195 | /// Closes the current sub-path with a line segment. |
| 196 | pub fn close(&mut self) { |
| 197 | self.append(&[PackedVerb::Close], &[]); |
| 198 | } |
| 199 | |
| 200 | /// Sets the current sub-path winding, see [`Solidity`]. |
| 201 | pub fn solidity(&mut self, solidity: Solidity) { |
| 202 | match solidity { |
| 203 | Solidity::Solid => self.append(&[PackedVerb::Solid], &[]), |
| 204 | Solidity::Hole => self.append(&[PackedVerb::Hole], &[]), |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | /// Creates new circle arc shaped sub-path. The arc center is at `cx`,`cy`, the arc radius is `r`, |
| 209 | /// and the arc is drawn from angle `a0` to `a1`, and swept in direction `dir` (Winding) |
| 210 | /// Angles are specified in radians. |
| 211 | pub fn arc(&mut self, cx: f32, cy: f32, r: f32, a0: f32, a1: f32, dir: Solidity) { |
| 212 | let cpos = Position { x: cx, y: cy }; |
| 213 | |
| 214 | let mut da = a1 - a0; |
| 215 | |
| 216 | if dir == Solidity::Hole { |
| 217 | if da.abs() >= PI * 2.0 { |
| 218 | da = PI * 2.0; |
| 219 | } else { |
| 220 | while da < 0.0 { |
| 221 | da += PI * 2.0 |
| 222 | } |
| 223 | } |
| 224 | } else if da.abs() >= PI * 2.0 { |
| 225 | da = -PI * 2.0; |
| 226 | } else { |
| 227 | while da > 0.0 { |
| 228 | da -= PI * 2.0 |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | // Split arc into max 90 degree segments. |
| 233 | let ndivs = ((da.abs() / (PI * 0.5) + 0.5) as i32).clamp(1, 5); |
| 234 | let hda = (da / ndivs as f32) / 2.0; |
| 235 | let mut kappa = (4.0 / 3.0 * (1.0 - hda.cos()) / hda.sin()).abs(); |
| 236 | |
| 237 | let mut commands = Vec::with_capacity(ndivs as usize); |
| 238 | let mut coords = Vec::with_capacity(ndivs as usize); |
| 239 | |
| 240 | if dir == Solidity::Solid { |
| 241 | kappa = -kappa; |
| 242 | } |
| 243 | |
| 244 | let (mut ppos, mut ptanpos) = (Position { x: 0.0, y: 0.0 }, Vector::zero()); |
| 245 | |
| 246 | for i in 0..=ndivs { |
| 247 | let a = a0 + da * (i as f32 / ndivs as f32); |
| 248 | let dpos = Vector::from_angle(a); |
| 249 | let pos = cpos + dpos * r; |
| 250 | let tanpos = -dpos.orthogonal() * r * kappa; |
| 251 | |
| 252 | if i == 0 { |
| 253 | let first_move = if self.verbs.is_empty() { |
| 254 | PackedVerb::MoveTo |
| 255 | } else { |
| 256 | PackedVerb::LineTo |
| 257 | }; |
| 258 | |
| 259 | commands.push(first_move); |
| 260 | coords.extend_from_slice(&[pos]); |
| 261 | } else { |
| 262 | commands.push(PackedVerb::BezierTo); |
| 263 | let pos1 = ppos + ptanpos; |
| 264 | let pos2 = pos - tanpos; |
| 265 | coords.extend_from_slice(&[pos1, pos2, pos]); |
| 266 | } |
| 267 | |
| 268 | ppos = pos; |
| 269 | ptanpos = tanpos; |
| 270 | } |
| 271 | |
| 272 | self.append(&commands, &coords); |
| 273 | } |
| 274 | |
| 275 | /// Adds an arc segment at the corner defined by the last path point and two specified points. |
| 276 | pub fn arc_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, radius: f32) { |
| 277 | if self.verbs.is_empty() { |
| 278 | return; |
| 279 | } |
| 280 | |
| 281 | let pos0 = self.last_pos; |
| 282 | let pos1 = Position { x: x1, y: y1 }; |
| 283 | let pos2 = Position { x: x2, y: y2 }; |
| 284 | |
| 285 | // Handle degenerate cases. |
| 286 | if Position::equals(pos0, pos1, self.dist_tol) |
| 287 | || Position::equals(pos1, pos2, self.dist_tol) |
| 288 | || Position::segment_distance(pos1, pos0, pos2) < self.dist_tol * self.dist_tol |
| 289 | || radius < self.dist_tol |
| 290 | { |
| 291 | self.line_to(pos1.x, pos1.y); |
| 292 | } |
| 293 | |
| 294 | let mut dpos0 = pos0 - pos1; |
| 295 | let mut dpos1 = pos2 - pos1; |
| 296 | |
| 297 | dpos0.normalize(); |
| 298 | dpos1.normalize(); |
| 299 | |
| 300 | let a = dpos0.dot(dpos1).acos(); |
| 301 | let d = radius / (a / 2.0).tan(); |
| 302 | |
| 303 | if d > 10000.0 { |
| 304 | return self.line_to(pos1.x, pos1.y); |
| 305 | } |
| 306 | |
| 307 | let (cpos, a0, a1, dir); |
| 308 | |
| 309 | if dpos0.cross(dpos1) > 0.0 { |
| 310 | cpos = pos1 + dpos0 * d + dpos0.orthogonal() * radius; |
| 311 | a0 = dpos0.angle(); |
| 312 | a1 = (-dpos1).angle(); |
| 313 | dir = Solidity::Hole; |
| 314 | } else { |
| 315 | cpos = pos1 + dpos0 * d - dpos0.orthogonal() * radius; |
| 316 | a0 = (-dpos0).angle(); |
| 317 | a1 = dpos1.angle(); |
| 318 | dir = Solidity::Solid; |
| 319 | } |
| 320 | |
| 321 | self.arc(cpos.x, cpos.y, radius, a0 + PI / 2.0, a1 + PI / 2.0, dir); |
| 322 | } |
| 323 | |
| 324 | /// Creates a new rectangle shaped sub-path. |
| 325 | pub fn rect(&mut self, x: f32, y: f32, w: f32, h: f32) { |
| 326 | self.append( |
| 327 | &[ |
| 328 | PackedVerb::MoveTo, |
| 329 | PackedVerb::LineTo, |
| 330 | PackedVerb::LineTo, |
| 331 | PackedVerb::LineTo, |
| 332 | PackedVerb::Close, |
| 333 | ], |
| 334 | &{ |
| 335 | let hoffset = Vector::x(w); |
| 336 | let voffset = Vector::y(h); |
| 337 | |
| 338 | let tl = Position { x, y }; |
| 339 | let tr = tl + hoffset; |
| 340 | let br = tr + voffset; |
| 341 | let bl = tl + voffset; |
| 342 | |
| 343 | [tl, bl, br, tr] |
| 344 | }, |
| 345 | ); |
| 346 | } |
| 347 | |
| 348 | /// Creates a new rounded rectangle shaped sub-path. |
| 349 | pub fn rounded_rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32) { |
| 350 | self.rounded_rect_varying(x, y, w, h, r, r, r, r); |
| 351 | } |
| 352 | |
| 353 | /// Creates a new rounded rectangle shaped sub-path with varying radii for each corner. |
| 354 | pub fn rounded_rect_varying( |
| 355 | &mut self, |
| 356 | x: f32, |
| 357 | y: f32, |
| 358 | w: f32, |
| 359 | h: f32, |
| 360 | rad_top_left: f32, |
| 361 | rad_top_right: f32, |
| 362 | rad_bottom_right: f32, |
| 363 | rad_bottom_left: f32, |
| 364 | ) { |
| 365 | if rad_top_left < 0.1 && rad_top_right < 0.1 && rad_bottom_right < 0.1 && rad_bottom_left < 0.1 { |
| 366 | self.rect(x, y, w, h); |
| 367 | } else { |
| 368 | let halfw = w.abs() * 0.5; |
| 369 | let halfh = h.abs() * 0.5; |
| 370 | |
| 371 | let rx_bl = rad_bottom_left.min(halfw) * w.signum(); |
| 372 | let ry_bl = rad_bottom_left.min(halfh) * h.signum(); |
| 373 | |
| 374 | let rx_br = rad_bottom_right.min(halfw) * w.signum(); |
| 375 | let ry_br = rad_bottom_right.min(halfh) * h.signum(); |
| 376 | |
| 377 | let rx_tr = rad_top_right.min(halfw) * w.signum(); |
| 378 | let ry_tr = rad_top_right.min(halfh) * h.signum(); |
| 379 | |
| 380 | let rx_tl = rad_top_left.min(halfw) * w.signum(); |
| 381 | let ry_tl = rad_top_left.min(halfh) * h.signum(); |
| 382 | |
| 383 | self.append( |
| 384 | &[ |
| 385 | PackedVerb::MoveTo, |
| 386 | PackedVerb::LineTo, |
| 387 | PackedVerb::BezierTo, |
| 388 | PackedVerb::LineTo, |
| 389 | PackedVerb::BezierTo, |
| 390 | PackedVerb::LineTo, |
| 391 | PackedVerb::BezierTo, |
| 392 | PackedVerb::LineTo, |
| 393 | PackedVerb::BezierTo, |
| 394 | PackedVerb::Close, |
| 395 | ], |
| 396 | &[ |
| 397 | Position { x, y: y + ry_tl }, |
| 398 | Position { x, y: y + h - ry_bl }, |
| 399 | // |
| 400 | Position { |
| 401 | x, |
| 402 | y: y + h - ry_bl * (1.0 - KAPPA90), |
| 403 | }, |
| 404 | Position { |
| 405 | x: x + rx_bl * (1.0 - KAPPA90), |
| 406 | y: y + h, |
| 407 | }, |
| 408 | Position { x: x + rx_bl, y: y + h }, |
| 409 | // |
| 410 | Position { |
| 411 | x: x + w - rx_br, |
| 412 | y: y + h, |
| 413 | }, |
| 414 | // |
| 415 | Position { |
| 416 | x: x + w - rx_br * (1.0 - KAPPA90), |
| 417 | y: y + h, |
| 418 | }, |
| 419 | Position { |
| 420 | x: x + w, |
| 421 | y: y + h - ry_br * (1.0 - KAPPA90), |
| 422 | }, |
| 423 | Position { |
| 424 | x: x + w, |
| 425 | y: y + h - ry_br, |
| 426 | }, |
| 427 | // |
| 428 | Position { x: x + w, y: y + ry_tr }, |
| 429 | // |
| 430 | Position { |
| 431 | x: x + w, |
| 432 | y: y + ry_tr * (1.0 - KAPPA90), |
| 433 | }, |
| 434 | Position { |
| 435 | x: x + w - rx_tr * (1.0 - KAPPA90), |
| 436 | y, |
| 437 | }, |
| 438 | Position { x: x + w - rx_tr, y }, |
| 439 | // |
| 440 | Position { x: x + rx_tl, y }, |
| 441 | // |
| 442 | Position { |
| 443 | x: x + rx_tl * (1.0 - KAPPA90), |
| 444 | y, |
| 445 | }, |
| 446 | Position { |
| 447 | x, |
| 448 | y: y + ry_tl * (1.0 - KAPPA90), |
| 449 | }, |
| 450 | Position { x, y: y + ry_tl }, |
| 451 | ], |
| 452 | ); |
| 453 | } |
| 454 | } |
| 455 | |
| 456 | /// Creates a new ellipse shaped sub-path. |
| 457 | pub fn ellipse(&mut self, cx: f32, cy: f32, rx: f32, ry: f32) { |
| 458 | self.append( |
| 459 | &[ |
| 460 | PackedVerb::MoveTo, |
| 461 | PackedVerb::BezierTo, |
| 462 | PackedVerb::BezierTo, |
| 463 | PackedVerb::BezierTo, |
| 464 | PackedVerb::BezierTo, |
| 465 | PackedVerb::Close, |
| 466 | ], |
| 467 | &{ |
| 468 | let cpos = Position { x: cx, y: cy }; |
| 469 | let hoffset = Vector::x(rx); |
| 470 | let voffset = Vector::y(ry); |
| 471 | [ |
| 472 | cpos - hoffset, |
| 473 | cpos - hoffset + voffset * KAPPA90, |
| 474 | cpos - hoffset * KAPPA90 + voffset, |
| 475 | cpos + voffset, |
| 476 | cpos + hoffset * KAPPA90 + voffset, |
| 477 | cpos + hoffset + voffset * KAPPA90, |
| 478 | cpos + hoffset, |
| 479 | cpos + hoffset - voffset * KAPPA90, |
| 480 | cpos + hoffset * KAPPA90 - voffset, |
| 481 | cpos - voffset, |
| 482 | cpos - hoffset * KAPPA90 - voffset, |
| 483 | cpos - hoffset - voffset * KAPPA90, |
| 484 | cpos - hoffset, |
| 485 | ] |
| 486 | }, |
| 487 | ); |
| 488 | } |
| 489 | |
| 490 | /// Creates a new circle shaped sub-path. |
| 491 | pub fn circle(&mut self, cx: f32, cy: f32, r: f32) { |
| 492 | self.ellipse(cx, cy, r, r); |
| 493 | } |
| 494 | |
| 495 | /// Appends a slice of verbs and coordinates to the path. |
| 496 | fn append(&mut self, verbs: &[PackedVerb], coords: &[Position]) { |
| 497 | if !coords.is_empty() { |
| 498 | self.last_pos = coords[coords.len() - 1]; |
| 499 | } |
| 500 | |
| 501 | self.verbs.extend_from_slice(verbs); |
| 502 | self.coords.extend_from_slice(coords); |
| 503 | } |
| 504 | } |
| 505 | |
| 506 | /// An iterator over the verbs and coordinates of a path. |
| 507 | pub struct PathIter<'a> { |
| 508 | verbs: slice::Iter<'a, PackedVerb>, |
| 509 | coords: &'a [Position], |
| 510 | } |
| 511 | |
| 512 | impl<'a> Iterator for PathIter<'a> { |
| 513 | type Item = Verb; |
| 514 | |
| 515 | fn next(&mut self) -> Option<Self::Item> { |
| 516 | if let Some(verb: &'a PackedVerb) = self.verbs.next() { |
| 517 | let verb: Verb = Verb::from_packed(packed:verb, self.coords); |
| 518 | let num_coords: usize = verb.num_coordinates(); |
| 519 | self.coords = &self.coords[num_coords..]; |
| 520 | Some(verb) |
| 521 | } else { |
| 522 | None |
| 523 | } |
| 524 | } |
| 525 | } |
| 526 | |
| 527 | impl ttf_parser::OutlineBuilder for Path { |
| 528 | fn move_to(&mut self, x: f32, y: f32) { |
| 529 | self.move_to(x, y); |
| 530 | } |
| 531 | |
| 532 | fn line_to(&mut self, x: f32, y: f32) { |
| 533 | self.line_to(x, y); |
| 534 | } |
| 535 | |
| 536 | fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { |
| 537 | self.quad_to(cx:x1, cy:y1, x, y); |
| 538 | } |
| 539 | |
| 540 | fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { |
| 541 | self.bezier_to(c1x:x1, c1y:y1, c2x:x2, c2y:y2, x, y); |
| 542 | } |
| 543 | |
| 544 | fn close(&mut self) { |
| 545 | self.close(); |
| 546 | } |
| 547 | } |
| 548 | |