1use super::{Line, Span, Style, Text};
5use bitflags::bitflags;
6use std::{
7 borrow::Cow,
8 mem::take,
9 sync::{
10 Arc,
11 atomic::{self, AtomicU32},
12 },
13};
14use unicode_segmentation::UnicodeSegmentation;
15use unicode_width::UnicodeWidthStr;
16
17use super::{injector::WorkerInjector, query::PickerQuery};
18use crate::{
19 SSS,
20 config::AutoscrollSettings,
21 nucleo::Render,
22 utils::text::{hscroll_indicator, text_to_string, wrap_text, wrapping_indicator},
23};
24
25type ColumnFormatFn<T> = Box<dyn for<'a> Fn(&'a T) -> Text<'a> + Send + Sync>;
26pub struct Column<T> {
27 pub name: Arc<str>,
28 pub(super) format: ColumnFormatFn<T>,
29 pub(super) filter: bool,
31}
32
33impl<T> Column<T> {
34 pub fn new_boxed(name: impl Into<Arc<str>>, format: ColumnFormatFn<T>) -> Self {
35 Self {
36 name: name.into(),
37 format,
38 filter: true,
39 }
40 }
41
42 pub fn new<F>(name: impl Into<Arc<str>>, f: F) -> Self
43 where
44 F: for<'a> Fn(&'a T) -> Text<'a> + SSS,
45 {
46 Self {
47 name: name.into(),
48 format: Box::new(f),
49 filter: true,
50 }
51 }
52
53 pub fn without_filtering(mut self) -> Self {
55 self.filter = false;
56 self
57 }
58
59 pub fn format<'a>(&self, item: &'a T) -> Text<'a> {
60 (self.format)(item)
61 }
62
63 pub fn format_text<'a>(&self, item: &'a T) -> Cow<'a, str> {
65 Cow::Owned(text_to_string(&(self.format)(item)))
66 }
67}
68
69pub struct Worker<T>
73where
74 T: SSS,
75{
76 pub nucleo: nucleo::Nucleo<T>,
78 pub query: PickerQuery,
80 pub col_indices_buffer: Vec<u32>,
83 pub columns: Arc<[Column<T>]>,
84
85 pub(super) version: Arc<AtomicU32>,
87 column_options: Vec<ColumnOptions>,
89}
90
91bitflags! {
97 #[derive(Default, Clone, Debug)]
98 pub struct ColumnOptions: u8 {
99 const Optional = 1 << 0;
100 const OrUseDefault = 1 << 2;
101 }
102}
103
104impl<T> Worker<T>
105where
106 T: SSS,
107{
108 pub fn new(columns: impl IntoIterator<Item = Column<T>>, default_column: usize) -> Self {
110 let columns: Arc<[_]> = columns.into_iter().collect();
111 let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
112
113 let inner = nucleo::Nucleo::new(
114 nucleo::Config::DEFAULT,
115 Arc::new(|| {}),
116 None,
117 matcher_columns,
118 );
119
120 Self {
121 nucleo: inner,
122 col_indices_buffer: Vec::with_capacity(128),
123 query: PickerQuery::new(columns.iter().map(|col| &col.name).cloned(), default_column),
124 column_options: vec![ColumnOptions::default(); columns.len()],
125 columns,
126 version: Arc::new(AtomicU32::new(0)),
127 }
128 }
129
130 #[cfg(feature = "experimental")]
131 pub fn set_column_options(&mut self, index: usize, options: ColumnOptions) {
132 if options.contains(ColumnOptions::Optional) {
133 self.nucleo
134 .pattern
135 .configure_column(index, nucleo::pattern::Variant::Optional)
136 }
137
138 self.column_options[index] = options
139 }
140
141 #[cfg(feature = "experimental")]
142 pub fn reverse_items(&mut self, reverse_items: bool) {
143 self.nucleo.reverse_items(reverse_items);
144 }
145
146 pub fn injector(&self) -> WorkerInjector<T> {
147 WorkerInjector {
148 inner: self.nucleo.injector(),
149 columns: self.columns.clone(),
150 version: self.version.load(atomic::Ordering::Relaxed),
151 picker_version: self.version.clone(),
152 }
153 }
154
155 pub fn find(&mut self, line: &str) {
156 let old_query = self.query.parse(line);
157 if self.query == old_query {
158 return;
159 }
160 for (i, column) in self
161 .columns
162 .iter()
163 .filter(|column| column.filter)
164 .enumerate()
165 {
166 let pattern = self
167 .query
168 .get(&column.name)
169 .map(|s| &**s)
170 .unwrap_or_else(|| {
171 self.column_options[i]
172 .contains(ColumnOptions::OrUseDefault)
173 .then(|| self.query.primary_column_query())
174 .flatten()
175 .unwrap_or_default()
176 });
177
178 let old_pattern = old_query
179 .get(&column.name)
180 .map(|s| &**s)
181 .unwrap_or_else(|| {
182 self.column_options[i]
183 .contains(ColumnOptions::OrUseDefault)
184 .then(|| {
185 let name = self.query.primary_column_name()?;
186 old_query.get(name).map(|s| &**s)
187 })
188 .flatten()
189 .unwrap_or_default()
190 });
191
192 if pattern == old_pattern {
194 continue;
195 }
196 let is_append = pattern.starts_with(old_pattern);
197
198 self.nucleo.pattern.reparse(
199 i,
200 pattern,
201 nucleo::pattern::CaseMatching::Smart,
202 nucleo::pattern::Normalization::Smart,
203 is_append,
204 );
205 }
206 }
207
208 pub fn get_nth(&self, n: u32) -> Option<&T> {
210 self.nucleo
211 .snapshot()
212 .get_matched_item(n)
213 .map(|item| item.data)
214 }
215
216 pub fn new_snapshot(nucleo: &mut nucleo::Nucleo<T>) -> (&nucleo::Snapshot<T>, Status) {
217 let nucleo::Status { changed, running } = nucleo.tick(10);
218 let snapshot = nucleo.snapshot();
219 (
220 snapshot,
221 Status {
222 item_count: snapshot.item_count(),
223 matched_count: snapshot.matched_item_count(),
224 running,
225 changed,
226 },
227 )
228 }
229
230 pub fn raw_results(&self) -> impl ExactSizeIterator<Item = &T> + DoubleEndedIterator + '_ {
231 let snapshot = self.nucleo.snapshot();
232 snapshot.matched_items(..).map(|item| item.data)
233 }
234
235 pub fn counts(&self) -> (u32, u32) {
237 let snapshot = self.nucleo.snapshot();
238 (snapshot.matched_item_count(), snapshot.item_count())
239 }
240
241 #[cfg(feature = "experimental")]
242 pub fn set_stability(&mut self, threshold: u32) {
243 self.nucleo.set_stability(threshold);
244 }
245
246 #[cfg(feature = "experimental")]
247 pub fn get_stability(&self) -> u32 {
248 self.nucleo.get_stability()
249 }
250
251 pub fn restart(&mut self, clear_snapshot: bool) {
252 self.nucleo.restart(clear_snapshot);
253 }
254}
255
256#[derive(Debug, Default, Clone)]
257pub struct Status {
258 pub item_count: u32,
259 pub matched_count: u32,
260 pub running: bool,
261 pub changed: bool,
262}
263
264#[derive(Debug, thiserror::Error)]
265pub enum WorkerError {
266 #[error("the matcher injector has been shut down")]
267 InjectorShutdown,
268 #[error("{0}")]
269 Custom(&'static str),
270}
271
272pub type WorkerResults<'a, T> = Vec<(Vec<Text<'a>>, &'a T)>;
274
275impl<T: SSS> Worker<T> {
276 pub fn results(
284 &mut self,
285 start: u32,
286 end: u32,
287 width_limits: &[u16],
288 wrap: bool,
289 highlight_style: Style,
290 matcher: &mut nucleo::Matcher,
291 autoscroll: AutoscrollSettings,
292 hscroll_offset: i8,
293 ) -> (WorkerResults<'_, T>, Vec<u16>, Status) {
294 let (snapshot, status) = Self::new_snapshot(&mut self.nucleo);
295
296 let mut widths = vec![0u16; self.columns.len()];
297
298 let iter =
299 snapshot.matched_items(start.min(status.matched_count)..end.min(status.matched_count));
300
301 let table = iter
302 .map(|item| {
303 let mut widths = widths.iter_mut();
304
305 let row = self
306 .columns
307 .iter()
308 .enumerate()
309 .zip(width_limits.iter().chain(std::iter::repeat(&u16::MAX)))
310 .map(|((col_idx, column), &width_limit)| {
311 let max_width = widths.next().unwrap();
312 let cell = column.format(item.data);
313
314 if width_limit == 0 {
316 return Text::default();
317 }
318
319 let (cell, width) = if column.filter {
320 render_cell(
321 cell,
322 col_idx,
323 snapshot,
324 &item,
325 matcher,
326 highlight_style,
327 wrap,
328 width_limit,
329 &mut self.col_indices_buffer,
330 autoscroll,
331 hscroll_offset,
332 )
333 } else if wrap {
335 let (cell, wrapped) = wrap_text(cell, width_limit.saturating_sub(1));
336
337 let width = if wrapped {
338 width_limit as usize
339 } else {
340 cell.width()
341 };
342 (cell, width)
343 } else {
344 let width = cell.width();
345 (cell, width)
346 };
347
348 if width as u16 > *max_width {
350 *max_width = width as u16;
351 }
352
353 cell
354 });
355
356 (row.collect(), item.data)
357 })
358 .collect();
359
360 for (w, c) in widths.iter_mut().zip(self.columns.iter()) {
362 let name_width = c.name.width() as u16;
363 if *w != 0 {
364 *w = (*w).max(name_width);
365 }
366 }
367
368 (table, widths, status)
369 }
370
371 pub fn exact_column_match(&mut self, column: &str) -> Option<&T> {
372 let (i, col) = self
373 .columns
374 .iter()
375 .enumerate()
376 .find(|(_, c)| column == &*c.name)?;
377
378 let query = self.query.get(column).map(|s| &**s).or_else(|| {
379 self.column_options[i]
380 .contains(ColumnOptions::OrUseDefault)
381 .then(|| self.query.primary_column_query())
382 .flatten()
383 })?;
384
385 let snapshot = self.nucleo.snapshot();
386 snapshot.matched_items(..).find_map(|item| {
387 let content = col.format_text(item.data);
388 if content.as_str() == query {
389 Some(item.data)
390 } else {
391 None
392 }
393 })
394 }
395
396 pub fn format_with<'a>(&'a self, item: &'a T, col: &str) -> Option<Cow<'a, str>> {
397 self.columns
398 .iter()
399 .find(|c| &*c.name == col)
400 .map(|c| c.format_text(item))
401 }
402}
403
404fn render_cell<T: SSS>(
405 cell: Text<'_>,
406 col_idx: usize,
407 snapshot: &nucleo::Snapshot<T>,
408 item: &nucleo::Item<T>,
409 matcher: &mut nucleo::Matcher,
410 highlight_style: Style,
411 wrap: bool,
412 width_limit: u16,
413 col_indices_buffer: &mut Vec<u32>,
414 mut autoscroll: AutoscrollSettings,
415 hscroll_offset: i8,
416) -> (Text<'static>, usize) {
417 autoscroll.end &= !wrap;
419
420 let mut cell_width = 0;
421 let mut wrapped = false;
422
423 let indices_buffer = col_indices_buffer;
425 indices_buffer.clear();
426 snapshot.pattern().column_pattern(col_idx).indices(
427 item.matcher_columns[col_idx].slice(..),
428 matcher,
429 indices_buffer,
430 );
431 indices_buffer.sort_unstable();
432 indices_buffer.dedup();
433 let mut indices = indices_buffer.drain(..);
434
435 let mut lines = vec![];
436 let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
437 let mut grapheme_idx = 0u32;
438
439 let mut line_graphemes = Vec::new();
440
441 for line in &cell {
442 line_graphemes.clear();
444 let mut match_idx = None;
445
446 for span in line {
447 for grapheme in span.content.graphemes(true) {
453 let is_match = grapheme_idx == next_highlight_idx;
454
455 let style = if is_match {
456 next_highlight_idx = indices.next().unwrap_or(u32::MAX);
457 span.style.patch(highlight_style)
458 } else {
459 span.style
460 };
461
462 if is_match && (autoscroll.end || match_idx.is_none()) {
463 match_idx = Some(line_graphemes.len());
464 }
465
466 line_graphemes.push((grapheme, style));
467 grapheme_idx += 1;
468 }
469 }
470
471 let mut i; if autoscroll.enabled && autoscroll.end {
475 i = match_idx.unwrap_or(line_graphemes.len());
476
477 let target_width = if let Some(x) = match_idx {
478 (width_limit as usize)
479 .saturating_sub(autoscroll.context.min(line_graphemes.len() - x - 1))
480 } else {
481 width_limit as usize
482 }
483 .saturating_sub(1);
484
485 let mut current_width = 0;
486
487 while i > 0 {
488 let w = line_graphemes[i - 1].0.width();
489 if current_width + w > target_width {
490 break;
491 }
492 i -= 1;
493 current_width += w;
494 }
495 if i > 1 {
496 i += 1;
497 } else {
498 i = 0;
499 }
500 } else if autoscroll.enabled
501 && let Some(m_idx) = match_idx
502 {
503 i = (m_idx as i32 + hscroll_offset as i32 - autoscroll.context as i32).max(0) as usize;
504
505 let mut tail_width: usize = line_graphemes[i..].iter().map(|(g, _)| g.width()).sum();
506
507 let preserved_width = line_graphemes
508 [..autoscroll.initial_preserved.min(line_graphemes.len())]
509 .iter()
510 .map(|(g, _)| g.width())
511 .sum::<usize>();
512
513 while i > autoscroll.initial_preserved {
515 let prev_width = line_graphemes[i - 1].0.width();
516 if tail_width + preserved_width + 1 + prev_width <= width_limit as usize {
517 i -= 1;
518 tail_width += prev_width;
519 } else {
520 break;
521 }
522 }
523
524 if i <= autoscroll.initial_preserved + 1 {
525 i = 0;
526 }
527 } else {
528 i = hscroll_offset.max(0) as usize;
529 };
530
531 let mut current_spans = Vec::new();
533 let mut current_span = String::new();
534 let mut current_style = Style::default();
535 let mut current_width = 0;
536
537 if i > 0 && autoscroll.enabled {
539 let preserved = autoscroll.initial_preserved;
540 for (g, s) in line_graphemes.drain(..preserved) {
541 if s != current_style {
542 if !current_span.is_empty() {
543 current_spans.push(Span::styled(current_span, current_style));
544 }
545 current_span = String::new();
546 current_style = s;
547 }
548 current_span.push_str(g);
549 }
550 if !current_span.is_empty() {
551 current_spans.push(Span::styled(current_span, current_style));
552 }
553 i -= preserved;
554
555 current_width += current_spans.iter().map(|x| x.width()).sum::<usize>();
556 current_spans.push(hscroll_indicator());
557 current_width += 1;
558
559 current_span = String::new();
560 current_style = Style::default();
561 }
562
563 let full_line_width = (!wrap).then(|| {
564 current_width
565 + line_graphemes[i..]
566 .iter()
567 .map(|(g, _)| g.width())
568 .sum::<usize>()
569 });
570
571 let mut graphemes = line_graphemes.drain(i..);
572
573 while let Some((mut grapheme, mut style)) = graphemes.next() {
574 if current_width + grapheme.width() > width_limit as usize {
575 if !current_span.is_empty() {
576 current_spans.push(Span::styled(current_span, current_style));
577 current_span = String::new();
578 }
579 if wrap {
580 current_spans.push(wrapping_indicator());
581 lines.push(Line::from(take(&mut current_spans)));
582
583 current_width = 0;
584 wrapped = true;
585 } else {
586 break;
587 }
588 } else if current_width + grapheme.width() == width_limit as usize {
589 if wrap {
590 let mut new = grapheme.to_string();
591 if current_style != style {
592 current_spans.push(Span::styled(take(&mut current_span), current_style));
593 current_style = style;
594 };
595 while let Some((grapheme2, style2)) = graphemes.next() {
596 if grapheme2.width() == 0 {
597 new.push_str(grapheme2);
598 } else {
599 if !current_span.is_empty() {
600 current_spans.push(Span::styled(current_span, current_style));
601 }
602 current_spans.push(wrapping_indicator());
603 lines.push(Line::from(take(&mut current_spans)));
604
605 current_span = new.clone(); current_width = grapheme.width();
608 wrapped = true;
609
610 grapheme = grapheme2;
611 style = style2;
612 break; }
614 }
615 if !wrapped {
616 current_span.push_str(&new);
617 current_spans.push(Span::styled(take(&mut current_span), style));
619 current_style = style;
620 current_width += grapheme.width();
621 break;
622 }
623 } else {
624 if style != current_style {
625 if !current_span.is_empty() {
626 current_spans.push(Span::styled(current_span, current_style));
627 }
628 current_span = String::new();
629 current_style = style;
630 }
631 current_span.push_str(grapheme);
632 current_width += grapheme.width();
633 break;
634 }
635 }
636
637 if style != current_style {
639 if !current_span.is_empty() {
640 current_spans.push(Span::styled(current_span, current_style))
641 }
642 current_span = String::new();
643 current_style = style;
644 }
645 current_span.push_str(grapheme);
646 current_width += grapheme.width();
647 }
648
649 current_spans.push(Span::styled(current_span, current_style));
650 lines.push(Line::from(current_spans));
651 cell_width = cell_width.max(full_line_width.unwrap_or(current_width));
652
653 grapheme_idx += 1; }
655
656 (
657 Text::from(lines),
658 if wrapped {
659 width_limit as usize
660 } else {
661 cell_width
662 },
663 )
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669 use nucleo::{Matcher, Nucleo};
670 use ratatui::style::{Color, Style};
671 use ratatui::text::Text;
672 use std::sync::Arc;
673
674 fn setup_nucleo_mocks(
676 search_query: &str,
677 item_text: &str,
678 ) -> (Nucleo<String>, Matcher, Vec<u32>) {
679 let mut nucleo = Nucleo::<String>::new(nucleo::Config::DEFAULT, Arc::new(|| {}), None, 1);
680
681 let injector = nucleo.injector();
682 injector.push(item_text.to_string(), |item, columns| {
683 columns[0] = item.clone().into();
684 });
685
686 nucleo.pattern.reparse(
687 0,
688 search_query,
689 nucleo::pattern::CaseMatching::Ignore,
690 nucleo::pattern::Normalization::Smart,
691 false,
692 );
693
694 nucleo.tick(10); let matcher = Matcher::default();
697 let buffer = Vec::new();
698
699 (nucleo, matcher, buffer)
700 }
701
702 #[test]
703 fn test_no_scroll_context_renders_normally() {
704 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
705 let snapshot = nucleo.snapshot();
706 let item = snapshot.get_item(0).unwrap();
707
708 let cell = Text::from("hello match world");
709 let highlight = Style::default().fg(Color::Red);
710
711 let (result_text, width) = render_cell(
712 cell,
713 0,
714 &snapshot,
715 &item,
716 &mut matcher,
717 highlight,
718 false,
719 u16::MAX,
720 &mut buffer,
721 AutoscrollSettings {
722 enabled: false,
723 ..Default::default()
724 },
725 0,
726 );
727
728 let output_str = text_to_string(&result_text);
729 assert_eq!(output_str, "hello match world");
730 assert_eq!(width, 17);
731 }
732
733 #[test]
734 fn test_scroll_context_cuts_prefix_correctly() {
735 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
736 let snapshot = nucleo.snapshot();
737 let item = snapshot.get_item(0).unwrap();
738
739 let cell = Text::from("hello match world");
740 let highlight = Style::default().fg(Color::Red);
741
742 let (result_text, _) = render_cell(
743 cell,
744 0,
745 &snapshot,
746 &item,
747 &mut matcher,
748 highlight,
749 false,
750 u16::MAX,
751 &mut buffer,
752 AutoscrollSettings {
753 initial_preserved: 0,
754 context: 2,
755 ..Default::default()
756 },
757 0,
758 );
759
760 let output_str = text_to_string(&result_text);
761 assert_eq!(output_str, "hello match world");
762 }
763
764 #[test]
765 fn test_scroll_context_backfills_to_fill_width_limit() {
766 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
781 let snapshot = nucleo.snapshot();
782 let item = snapshot.get_item(0).unwrap();
783
784 let cell = Text::from("abcdefghijmatch");
785 let highlight = Style::default().fg(Color::Red);
786
787 let (result_text, width) = render_cell(
788 cell,
789 0,
790 &snapshot,
791 &item,
792 &mut matcher,
793 highlight,
794 false,
795 10,
796 &mut buffer,
797 AutoscrollSettings {
798 initial_preserved: 0,
799 context: 1,
800 ..Default::default()
801 },
802 0,
803 );
804
805 let output_str = text_to_string(&result_text);
806 assert_eq!(output_str, "…ghijmatch");
807 assert_eq!(width, 10);
808 }
809
810 #[test]
811 fn test_preserved_prefix_and_ellipsis() {
812 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
825 let snapshot = nucleo.snapshot();
826 let item = snapshot.get_item(0).unwrap();
827
828 let cell = Text::from("abcdefghijmatch");
829 let highlight = Style::default().fg(Color::Red);
830
831 let (result_text, width) = render_cell(
832 cell,
833 0,
834 &snapshot,
835 &item,
836 &mut matcher,
837 highlight,
838 false,
839 10,
840 &mut buffer,
841 AutoscrollSettings {
842 initial_preserved: 3,
843 context: 1,
844 ..Default::default()
845 },
846 0,
847 );
848
849 let output_str = text_to_string(&result_text);
850 assert_eq!(output_str, "abc…jmatch");
851 assert_eq!(width, 10);
852 }
853
854 #[test]
855 fn test_wrap() {
856 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefmatch");
857 let snapshot = nucleo.snapshot();
858 let item = snapshot.get_item(0).unwrap();
859
860 let cell = Text::from("abcdefmatch");
861 let highlight = Style::default().fg(Color::Red);
862
863 let (result_text, width) = render_cell(
864 cell,
865 0,
866 &snapshot,
867 &item,
868 &mut matcher,
869 highlight,
870 true,
871 10,
872 &mut buffer,
873 AutoscrollSettings {
874 initial_preserved: 3,
875 context: 1,
876 ..Default::default()
877 },
878 -2,
879 );
880
881 let output_str = text_to_string(&result_text);
882 assert_eq!(output_str, "abcdefmat↵\nch");
883 assert_eq!(width, 10);
884 }
885
886 #[test]
887 fn test_wrap_edge_case_6_chars_width_5() {
888 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("", "123456");
889 let snapshot = nucleo.snapshot();
890 let item = snapshot.get_item(0).unwrap();
891
892 let cell = Text::from("123456");
893 let highlight = Style::default().fg(Color::Red);
894
895 let (result_text, width) = render_cell(
896 cell,
897 0,
898 &snapshot,
899 &item,
900 &mut matcher,
901 highlight,
902 true,
903 5,
904 &mut buffer,
905 AutoscrollSettings {
906 enabled: false,
907 ..Default::default()
908 },
909 0,
910 );
911
912 let output_str = text_to_string(&result_text);
913 assert_eq!(output_str, "1234↵\n56");
915 assert_eq!(width, 5);
916 }
917
918 #[test]
919 fn test_autoscroll_end() {
920 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
921 let snapshot = nucleo.snapshot();
922 let item = snapshot.get_item(0).unwrap();
923
924 let cell = Text::from("abcdefghijmatch");
925 let highlight = Style::default().fg(Color::Red);
926
927 let (result_text, width) = render_cell(
928 cell,
929 0,
930 &snapshot,
931 &item,
932 &mut matcher,
933 highlight,
934 false,
935 10,
936 &mut buffer,
937 AutoscrollSettings {
938 end: true,
939 context: 4,
940 ..Default::default()
941 },
942 0,
943 );
944
945 let output_str = text_to_string(&result_text);
946 assert_eq!(output_str, "…ghijmatch");
947 assert_eq!(width, 10);
948 }
949}