use gtk4::cairo::Context;
use gtk4::pango;
use super::{cairo_rgb, set_source};
use crate::primitives::data_table::{ColumnAlign, DataTable, DataTableLayout, SortDirection};
use crate::primitives::layout_metrics::pixel_data_table_layout;
use crate::theme::Theme;
const SORT_GAP: f64 = 4.0;
#[allow(clippy::too_many_arguments)]
pub fn draw_data_table(
cr: &Context,
pango_layout: &pango::Layout,
x: f64,
y: f64,
width: f64,
height: f64,
table: &DataTable,
theme: &Theme,
line_height: f64,
hovered_idx: Option<usize>,
) -> DataTableLayout {
draw_data_table_with_glyphs(
cr,
pango_layout,
x,
y,
width,
height,
table,
theme,
line_height,
hovered_idx,
None,
false,
)
}
#[allow(clippy::too_many_arguments)]
pub fn draw_data_table_with_glyphs(
cr: &Context,
pango_layout: &pango::Layout,
x: f64,
y: f64,
width: f64,
height: f64,
table: &DataTable,
theme: &Theme,
line_height: f64,
hovered_idx: Option<usize>,
glyphs: Option<&crate::primitives::data_table::DataTableGlyphs>,
nerd_fonts_enabled: bool,
) -> DataTableLayout {
pango_layout.set_attributes(None);
let measure = super::toolbar::PangoMeasure {
pango_layout: Some(pango_layout),
char_width: 0.0,
};
let layout = pixel_data_table_layout(
table,
width as f32,
height as f32,
line_height as f32,
&measure,
);
let header_height = layout.header_height as f64;
if width <= 0.0 || height <= 0.0 {
return layout;
}
cr.save().ok();
cr.rectangle(x, y, width, height);
cr.clip();
let (hbr, hbg, hbb) = cairo_rgb(theme.tab_bar_bg);
cr.set_source_rgb(hbr, hbg, hbb);
cr.rectangle(x, y, width, header_height);
cr.fill().ok();
let h_off = table.h_scroll as f64;
set_source(cr, theme.foreground);
let bold_attrs = pango::AttrList::new();
bold_attrs.insert(pango::AttrInt::new_weight(pango::Weight::Bold));
let base_font = pango_layout.font_description().unwrap_or_default();
let mut bold_icon_font = base_font.clone();
bold_icon_font.set_weight(pango::Weight::Bold);
let bold_icon_font = super::with_nerd_font_fallback(&bold_icon_font);
let sort_ascending_glyph: std::borrow::Cow<'_, str> = glyphs
.and_then(crate::primitives::data_table::DataTableGlyphs::sort_ascending_glyph)
.map(|icon| std::borrow::Cow::Borrowed(icon.resolve(nerd_fonts_enabled)))
.unwrap_or(std::borrow::Cow::Owned(
crate::codicon::SORT_ASCENDING.to_string(),
));
let sort_descending_glyph: std::borrow::Cow<'_, str> = glyphs
.and_then(crate::primitives::data_table::DataTableGlyphs::sort_descending_glyph)
.map(|icon| std::borrow::Cow::Borrowed(icon.resolve(nerd_fonts_enabled)))
.unwrap_or(std::borrow::Cow::Owned(
crate::codicon::SORT_DESCENDING.to_string(),
));
for (col_idx, rc) in layout.columns.iter().enumerate() {
if col_idx >= table.columns.len() || rc.width <= 0.0 {
break;
}
let col = &table.columns[col_idx];
let sort_glyph: Option<&str> = match &table.sort {
Some((si, dir)) if *si == col_idx => Some(match dir {
SortDirection::Ascending => sort_ascending_glyph.as_ref(),
SortDirection::Descending => sort_descending_glyph.as_ref(),
}),
_ => None,
};
pango_layout.set_font_description(Some(&base_font));
pango_layout.set_text(&col.title);
pango_layout.set_attributes(Some(&bold_attrs));
let (title_w, _) = pango_layout.pixel_size();
let sort_glyph_str = sort_glyph.map(|g| g.to_string());
let sort_w = if let Some(ref g) = sort_glyph_str {
pango_layout.set_font_description(Some(&bold_icon_font));
pango_layout.set_attributes(None);
pango_layout.set_text(g);
pango_layout.pixel_size().0 as f64
} else {
0.0
};
let text_w = title_w as f64
+ if sort_glyph_str.is_some() {
SORT_GAP + sort_w
} else {
0.0
};
let col_x = x + rc.x as f64 - h_off;
let col_w = rc.width as f64;
cr.save().ok();
cr.rectangle(col_x, y, col_w, header_height);
cr.clip();
let text_x = match col.align {
ColumnAlign::Left => col_x,
ColumnAlign::Center => col_x + (col_w - text_w) / 2.0,
ColumnAlign::Right => col_x + col_w - text_w,
};
pango_layout.set_font_description(Some(&base_font));
pango_layout.set_attributes(Some(&bold_attrs));
pango_layout.set_text(&col.title);
cr.move_to(text_x, y);
super::painted_text::show_layout(cr, pango_layout);
if let Some(ref g) = sort_glyph_str {
pango_layout.set_font_description(Some(&bold_icon_font));
pango_layout.set_attributes(None);
pango_layout.set_text(g);
cr.move_to(text_x + title_w as f64 + SORT_GAP, y);
super::painted_text::show_layout(cr, pango_layout);
}
cr.restore().ok();
}
pango_layout.set_font_description(Some(&base_font));
pango_layout.set_attributes(None);
set_source(cr, theme.separator);
for (col_idx, rc) in layout.columns.iter().enumerate() {
if col_idx + 1 >= layout.columns.len() {
break;
}
let sep_x = x + (rc.x + rc.width) as f64 - h_off;
cr.rectangle(sep_x - 0.5, y, 1.0, header_height);
cr.fill().ok();
}
let body_y = y + header_height;
let visible = layout
.visible_rows
.min(table.rows.len().saturating_sub(table.scroll_offset));
for row_idx in 0..visible {
let abs_idx = table.scroll_offset + row_idx;
let row = &table.rows[abs_idx];
let row_y = body_y + row_idx as f64 * line_height;
let is_selected = table.selected_idx == Some(abs_idx);
let is_hovered = hovered_idx == Some(abs_idx) && !is_selected;
if is_selected {
let (sr, sg, sb) = cairo_rgb(theme.selection_bg);
cr.set_source_rgba(sr, sg, sb, theme.selection_alpha as f64);
cr.rectangle(x, row_y, width, line_height);
cr.fill().ok();
} else if is_hovered {
let (hr, hg, hb) = cairo_rgb(theme.tab_bar_bg);
cr.set_source_rgba(hr, hg, hb, 0.5);
cr.rectangle(x, row_y, width, line_height);
cr.fill().ok();
}
for (col_idx, rc) in layout.columns.iter().enumerate() {
let styled = match row.cells.get(col_idx) {
Some(c) if !c.spans.is_empty() => c,
_ => continue,
};
if rc.width <= 0.0 {
continue;
}
let col_w = rc.width as f64;
let col_x = x + rc.x as f64 - h_off;
let is_muted = row.decoration == crate::types::Decoration::Muted;
cr.save().ok();
cr.rectangle(col_x, row_y, col_w, line_height);
cr.clip();
let full_text: String = styled.spans.iter().map(|s| s.text.as_str()).collect();
pango_layout.set_text(&full_text);
pango_layout.set_attributes(None);
let (text_w, _) = pango_layout.pixel_size();
let align = table
.columns
.get(col_idx)
.map(|c| c.align)
.unwrap_or(ColumnAlign::Left);
let text_x = match align {
ColumnAlign::Left => col_x,
ColumnAlign::Center => col_x + (col_w - text_w as f64) / 2.0,
ColumnAlign::Right => col_x + col_w - text_w as f64,
};
let attrs = pango::AttrList::new();
let mut byte_offset = 0u32;
for span in &styled.spans {
let span_bytes = span.text.len() as u32;
if !is_muted {
if let Some(fg) = span.fg {
let mut color_attr = pango::AttrColor::new_foreground(
fg.r as u16 * 257,
fg.g as u16 * 257,
fg.b as u16 * 257,
);
color_attr.set_start_index(byte_offset);
color_attr.set_end_index(byte_offset + span_bytes);
attrs.insert(color_attr);
}
}
byte_offset += span_bytes;
}
if is_muted {
set_source(cr, theme.muted_fg);
} else {
set_source(cr, theme.foreground);
}
pango_layout.set_attributes(Some(&attrs));
cr.move_to(text_x, row_y);
super::painted_text::show_layout(cr, pango_layout);
pango_layout.set_attributes(None);
cr.restore().ok();
}
set_source(cr, theme.separator);
for (col_idx, rc) in layout.columns.iter().enumerate() {
if col_idx + 1 >= layout.columns.len() || rc.width <= 0.0 {
continue;
}
let sep_x = x + (rc.x + rc.width) as f64 - h_off;
cr.rectangle(sep_x - 0.5, row_y, 1.0, line_height);
cr.fill().ok();
}
}
let footer_h = layout.footer_height as f64;
if table.show_scrollbar
&& table.rows.len() > layout.visible_rows
&& layout.scrollbar_width > 0.0
{
let sb_x = x + width - layout.scrollbar_width as f64;
let sb_track = crate::event::Rect::new(
sb_x as f32,
(y + header_height) as f32,
layout.scrollbar_width,
(height - header_height - footer_h).max(0.0) as f32,
);
let sb = crate::primitives::scrollbar::Scrollbar::vertical(
table.id,
sb_track,
table.scroll_offset as f32,
table.rows.len() as f32,
layout.visible_rows as f32,
line_height as f32,
);
let mut raw = super::surface::CairoSurface {
cr,
layout: None,
translucent_fill: true,
};
crate::primitives::scrollbar::native_surface_paint::paint(&sb, &mut raw, theme);
}
if layout.h_scrollbar_height > 0.0 && layout.content_width > 0.0 {
let hsb_y = y + height - footer_h - layout.h_scrollbar_height as f64;
let track_w = (width - layout.scrollbar_width as f64).max(1.0);
let hsb_track = crate::event::Rect::new(
x as f32,
hsb_y as f32,
track_w as f32,
layout.h_scrollbar_height,
);
let visible_w = track_w as f32;
let hsb = crate::primitives::scrollbar::Scrollbar::horizontal(
table.id,
hsb_track,
table.h_scroll,
layout.content_width,
visible_w,
line_height as f32,
);
let mut raw = super::surface::CairoSurface {
cr,
layout: None,
translucent_fill: true,
};
crate::primitives::scrollbar::native_surface_paint::paint(&hsb, &mut raw, theme);
}
if let Some(footer) = &table.footer {
if footer_h > 0.0 {
let footer_band_top = y + height - footer_h;
let footer_y = y + height - line_height;
set_source(cr, theme.separator);
cr.rectangle(x, footer_band_top - 0.5, width, 1.0);
cr.fill().ok();
let (fbr, fbg, fbb) = cairo_rgb(theme.tab_bar_bg);
cr.set_source_rgb(fbr, fbg, fbb);
cr.rectangle(x, footer_band_top, width, footer_h);
cr.fill().ok();
for (col_idx, rc) in layout.columns.iter().enumerate() {
let styled = match footer.cells.get(col_idx) {
Some(c) if !c.spans.is_empty() => c,
_ => continue,
};
if rc.width <= 0.0 {
continue;
}
let col_w = rc.width as f64;
let col_x = x + rc.x as f64 - h_off;
cr.save().ok();
cr.rectangle(col_x, footer_y, col_w, line_height);
cr.clip();
let full_text: String = styled.spans.iter().map(|s| s.text.as_str()).collect();
pango_layout.set_text(&full_text);
pango_layout.set_attributes(Some(&bold_attrs));
let (text_w, _) = pango_layout.pixel_size();
let align = table
.columns
.get(col_idx)
.map(|c| c.align)
.unwrap_or(ColumnAlign::Left);
let text_x = match align {
ColumnAlign::Left => col_x,
ColumnAlign::Center => col_x + (col_w - text_w as f64) / 2.0,
ColumnAlign::Right => col_x + col_w - text_w as f64,
};
let attrs = pango::AttrList::new();
attrs.insert(pango::AttrInt::new_weight(pango::Weight::Bold));
let mut byte_offset = 0u32;
for span in &styled.spans {
let span_bytes = span.text.len() as u32;
if let Some(fg) = span.fg {
let mut color_attr = pango::AttrColor::new_foreground(
fg.r as u16 * 257,
fg.g as u16 * 257,
fg.b as u16 * 257,
);
color_attr.set_start_index(byte_offset);
color_attr.set_end_index(byte_offset + span_bytes);
attrs.insert(color_attr);
}
byte_offset += span_bytes;
}
set_source(cr, theme.foreground);
pango_layout.set_attributes(Some(&attrs));
cr.move_to(text_x, footer_y);
super::painted_text::show_layout(cr, pango_layout);
pango_layout.set_attributes(None);
cr.restore().ok();
}
}
}
cr.restore().ok();
layout
}
pub fn gtk_data_table_layout(
pango_layout: &pango::Layout,
table: &DataTable,
width: f64,
height: f64,
line_height: f64,
) -> DataTableLayout {
pango_layout.set_attributes(None);
let measure = super::toolbar::PangoMeasure {
pango_layout: Some(pango_layout),
char_width: 0.0,
};
pixel_data_table_layout(
table,
width as f32,
height as f32,
line_height as f32,
&measure,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::data_table::{Column, ColumnWidth, DataRow};
use crate::theme::Theme;
use crate::types::{Color, Decoration, StyledText, WidgetId};
use pangocairo::cairo::{Context, Format, ImageSurface};
const W: i32 = 300;
const H: i32 = 100;
const LINE_H: f64 = 16.0;
fn pixel(data: &[u8], stride: usize, x: i32, y: i32) -> (u8, u8, u8) {
let off = y as usize * stride + x as usize * 4;
(data[off + 2], data[off + 1], data[off])
}
fn rgb(c: Color) -> (u8, u8, u8) {
(c.r, c.g, c.b)
}
fn three_col_table(a: &str, b: &str, c: &str) -> DataTable {
DataTable {
id: WidgetId::new("clip-test"),
columns: vec![
Column {
title: "A".into(),
width: ColumnWidth::Flex(1.0),
align: ColumnAlign::Left,
},
Column {
title: "B".into(),
width: ColumnWidth::Flex(1.0),
align: ColumnAlign::Left,
},
Column {
title: "C".into(),
width: ColumnWidth::Flex(1.0),
align: ColumnAlign::Left,
},
],
rows: vec![DataRow {
cells: vec![
StyledText::plain(a),
StyledText::plain(b),
StyledText::plain(c),
],
decoration: Decoration::Normal,
}],
selected_idx: None,
scroll_offset: 0,
sort: None,
has_focus: false,
show_scrollbar: false,
min_total_width: None,
h_scroll: 0.0,
column_overrides: Vec::new(),
footer: None,
}
}
fn paint(table: &DataTable, theme: &Theme) -> (Vec<u8>, i32, DataTableLayout) {
let mut surface = ImageSurface::create(Format::ARgb32, W, H).expect("create ImageSurface");
let table_layout;
{
let cr = Context::new(&surface).expect("Context::new");
let (br, bg, bb) = cairo_rgb(theme.background);
cr.set_source_rgb(br, bg, bb);
cr.paint().ok();
let pango_layout = pangocairo::functions::create_layout(&cr);
table_layout = draw_data_table(
&cr,
&pango_layout,
0.0,
0.0,
W as f64,
H as f64,
table,
theme,
LINE_H,
None,
);
}
surface.flush();
let stride = surface.stride();
let data = surface.data().expect("surface data").to_vec();
(data, stride, table_layout)
}
#[test]
fn body_cell_wider_than_column_does_not_paint_into_neighbour() {
let long = "this-value-is-far-wider-than-its-resolved-column-share-by-a-lot";
let table = three_col_table("A", long, "C");
let theme = Theme::default();
let (data, stride, layout) = paint(&table, &theme);
let c_col = &layout.columns[2];
let probe_x = (c_col.x + c_col.width - 4.0) as i32;
let probe_y = (layout.header_height as f64 + LINE_H / 2.0) as i32;
let px = pixel(&data, stride as usize, probe_x, probe_y);
assert_eq!(
px,
rgb(theme.background),
"pixel inside column C ({probe_x},{probe_y}) should be background, \
not B's overflow text bleeding through the clip"
);
}
#[test]
fn sorted_column_paints_a_glyph_after_its_title_unsorted_does_not() {
let theme = Theme::default();
let mut sorted = three_col_table("A", "B", "C");
sorted.sort = Some((0, SortDirection::Ascending));
let (sorted_data, stride, layout) = paint(&sorted, &theme);
let unsorted = three_col_table("A", "B", "C");
let (unsorted_data, stride2, _) = paint(&unsorted, &theme);
assert_eq!(stride, stride2, "same canvas size, same stride");
let col0 = &layout.columns[0];
let x0 = col0.x as i32;
let x1 = (col0.x + col0.width - 2.0) as i32;
let header_y_lo = 1_i32;
let header_y_hi = (layout.header_height as i32 - 1).max(header_y_lo + 1);
let differs_at = |x: i32| {
(header_y_lo..header_y_hi).any(|y| {
pixel(&sorted_data, stride as usize, x, y)
!= pixel(&unsorted_data, stride2 as usize, x, y)
})
};
assert!(
!differs_at(x0),
"column 0's leading edge (under the title's own first glyph) must render \
identically whether or not it is the sort column"
);
assert!(
(x0..x1).any(differs_at),
"sorting column 0 should paint a glyph somewhere in x={x0}..{x1} that the \
unsorted render does not"
);
}
#[test]
fn body_row_draws_separator_at_same_x_as_header() {
let table = three_col_table("A", "B", "C");
let theme = Theme::default();
let (data, stride, layout) = paint(&table, &theme);
let sep_x = (layout.columns[0].x + layout.columns[0].width) as i32;
let header_y = (layout.header_height as f64 / 2.0) as i32;
let body_y = (layout.header_height as f64 + LINE_H / 2.0) as i32;
let header_px = pixel(&data, stride as usize, sep_x, header_y);
let body_px = pixel(&data, stride as usize, sep_x, body_y);
assert_ne!(
header_px,
rgb(theme.background),
"sanity check: header separator pixel should differ from background"
);
assert_eq!(
body_px, header_px,
"body row should draw the same separator at sep_x={sep_x} the header uses: \
header pixel {header_px:?}, body pixel {body_px:?}"
);
assert_ne!(
body_px,
rgb(theme.background),
"separator pixel must differ from the row background"
);
}
#[test]
fn separator_survives_a_cell_that_fills_its_entire_column() {
let filling = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
let table = three_col_table(filling, "B", "C");
let theme = Theme::default();
let (data, stride, layout) = paint(&table, &theme);
let sep_x = (layout.columns[0].x + layout.columns[0].width) as i32;
let header_y = (layout.header_height as f64 / 2.0) as i32;
let body_y = (layout.header_height as f64 + LINE_H / 2.0) as i32;
let header_px = pixel(&data, stride as usize, sep_x, header_y);
let body_px = pixel(&data, stride as usize, sep_x, body_y);
assert_eq!(
body_px, header_px,
"separator must survive even when the left column's text fills the \
entire column width: header pixel {header_px:?}, body pixel {body_px:?}"
);
}
}
#[cfg(test)]
mod multi_select_driver_tests {
use crate::gtk::testing::GtkDriver;
use crate::primitives::data_table::ColumnMeasure;
use crate::runner::{AppLogic, Reaction};
use crate::{
apply_selection_gesture, Backend, Column, DataRow, DataTable, DataTableEvent, DataTableHit,
Decoration, Modifiers, MouseButton, Point, Rect, SelectionGesture, StyledText, WidgetId,
};
const VIEWPORT_W: f32 = 40.0;
const VIEWPORT_H: f32 = 10.0;
const ROW_H: f32 = 1.0;
const HEADER_H: f32 = 1.0;
const SCROLLBAR_W: f32 = 1.0;
fn row(label: &str) -> DataRow {
DataRow {
cells: vec![StyledText::plain(label.to_string())],
decoration: Decoration::Normal,
}
}
fn sample_table() -> DataTable {
DataTable {
id: WidgetId::new("dt"),
columns: vec![Column {
title: "Name".to_string(),
width: Default::default(),
align: Default::default(),
}],
rows: (0..5).map(|i| row(&format!("row {i}"))).collect(),
selected_idx: None,
scroll_offset: 0,
sort: None,
has_focus: true,
show_scrollbar: false,
min_total_width: None,
h_scroll: 0.0,
column_overrides: vec![],
footer: None,
}
}
struct TableApp {
table: DataTable,
selected: Vec<usize>,
anchor: usize,
events: Vec<DataTableEvent>,
}
impl TableApp {
fn new() -> Self {
Self {
table: sample_table(),
selected: Vec::new(),
anchor: 0,
events: Vec::new(),
}
}
}
impl AppLogic for TableApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
backend.draw_data_table(
Rect::new(0.0, 0.0, VIEWPORT_W, VIEWPORT_H),
&self.table,
None,
);
}
fn handle(&mut self, event: crate::UiEvent, _backend: &mut dyn Backend) -> Reaction {
let crate::UiEvent::MouseDown {
position,
modifiers,
..
} = event
else {
return Reaction::Continue;
};
let layout =
self.table
.layout(VIEWPORT_W, VIEWPORT_H, ROW_H, HEADER_H, SCROLLBAR_W, |_| {
ColumnMeasure::new(20.0)
});
let hit = layout.hit_test(
position.x,
position.y,
self.table.scroll_offset,
self.table.rows.len(),
);
let idx = match hit {
DataTableHit::Row { idx } => idx,
_ => return Reaction::Continue,
};
let gesture = SelectionGesture::from_modifiers(modifiers);
let indices = apply_selection_gesture(&self.selected, self.anchor, idx, gesture);
self.selected = indices.clone();
if !matches!(gesture, SelectionGesture::Range) {
self.anchor = idx;
}
self.events.push(DataTableEvent::SelectionSet { indices });
Reaction::Redraw
}
}
fn mouse_down(x: f32, y: f32, modifiers: Modifiers) -> crate::UiEvent {
crate::UiEvent::MouseDown {
widget: None,
button: MouseButton::Left,
position: Point::new(x, y),
modifiers,
}
}
#[test]
fn plain_click_replaces_selection() {
let mut driver = GtkDriver::new(TableApp::new(), 80, 40);
driver.dispatch(mouse_down(
2.0,
HEADER_H + 2.0 * ROW_H,
Modifiers::default(),
));
assert_eq!(driver.app().selected, vec![2]);
}
#[test]
fn ctrl_click_toggles_rows_into_a_multi_selection() {
let mut driver = GtkDriver::new(TableApp::new(), 80, 40);
driver.set_double_click_folding(false);
driver.dispatch(mouse_down(2.0, HEADER_H, Modifiers::default()));
let ctrl = Modifiers {
ctrl: true,
..Default::default()
};
driver.dispatch(mouse_down(2.0, HEADER_H + 2.0 * ROW_H, ctrl));
assert_eq!(driver.app().selected, vec![0, 2]);
driver.dispatch(mouse_down(2.0, HEADER_H + 2.0 * ROW_H, ctrl));
assert_eq!(driver.app().selected, vec![0]);
assert_eq!(
driver.app().events.last(),
Some(&DataTableEvent::SelectionSet { indices: vec![0] })
);
}
#[test]
fn shift_click_selects_the_range_from_the_anchor() {
let mut driver = GtkDriver::new(TableApp::new(), 80, 40);
driver.set_double_click_folding(false);
driver.dispatch(mouse_down(2.0, HEADER_H, Modifiers::default()));
let shift = Modifiers {
shift: true,
..Default::default()
};
driver.dispatch(mouse_down(2.0, HEADER_H + 3.0 * ROW_H, shift));
assert_eq!(driver.app().selected, vec![0, 1, 2, 3]);
assert_eq!(
driver.app().events.last(),
Some(&DataTableEvent::SelectionSet {
indices: vec![0, 1, 2, 3]
})
);
}
#[test]
fn cmd_click_toggles_the_same_as_ctrl_click() {
let mut driver = GtkDriver::new(TableApp::new(), 80, 40);
driver.set_double_click_folding(false);
driver.dispatch(mouse_down(2.0, HEADER_H + ROW_H, Modifiers::default()));
driver.dispatch(mouse_down(
2.0,
HEADER_H + 4.0 * ROW_H,
Modifiers {
cmd: true,
..Default::default()
},
));
assert_eq!(driver.app().selected, vec![1, 4]);
}
}