table: Improve Table to only render visible range columns for performance up to stable on 120 FPS (#489)
This change to make Table to only render visible range columns. With the example table story case: - 32 columns ## Before - Normally fast scroll: 80~90 FPS - Drag fast scroll: 60 FPS ## After - Normally fast scroll: 120 FPS - Drag fast scroll: 90~100 FPS 
This commit is contained in:
parent
64a3b4399b
commit
8558da4a7b
4 changed files with 332 additions and 31 deletions
|
|
@ -301,11 +301,13 @@ impl TableDelegate for StockTableDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn col_width(&self, col_ix: usize, _: &AppContext) -> Option<Pixels> {
|
fn col_width(&self, col_ix: usize, _: &AppContext) -> Pixels {
|
||||||
if let Some(_) = self.columns.get(col_ix) {
|
if col_ix < 10 {
|
||||||
Some(120.0.into())
|
120.0.into()
|
||||||
|
} else if col_ix < 20 {
|
||||||
|
80.0.into()
|
||||||
} else {
|
} else {
|
||||||
None
|
130.0.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ mod icon;
|
||||||
mod root;
|
mod root;
|
||||||
mod styled;
|
mod styled;
|
||||||
mod svg_img;
|
mod svg_img;
|
||||||
|
mod table_row;
|
||||||
mod time;
|
mod time;
|
||||||
mod title_bar;
|
mod title_bar;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use crate::{
|
||||||
h_flex,
|
h_flex,
|
||||||
popup_menu::PopupMenu,
|
popup_menu::PopupMenu,
|
||||||
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
|
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
|
||||||
|
table_row::table_row,
|
||||||
theme::ActiveTheme,
|
theme::ActiveTheme,
|
||||||
v_flex, Icon, IconName, Sizable, Size, StyleSized as _,
|
v_flex, Icon, IconName, Sizable, Size, StyleSized as _,
|
||||||
};
|
};
|
||||||
|
|
@ -45,19 +46,19 @@ pub enum ColFixed {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct ColGroup {
|
pub(crate) struct ColGroup {
|
||||||
width: Option<Pixels>,
|
pub(crate) width: Pixels,
|
||||||
bounds: Bounds<Pixels>,
|
pub(crate) bounds: Bounds<Pixels>,
|
||||||
sort: Option<ColSort>,
|
pub(crate) sort: Option<ColSort>,
|
||||||
fixed: Option<ColFixed>,
|
pub(crate) fixed: Option<ColFixed>,
|
||||||
padding: Option<Edges<Pixels>>,
|
pub(crate) padding: Option<Edges<Pixels>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct DragCol {
|
pub(crate) struct DragCol {
|
||||||
pub(crate) entity_id: EntityId,
|
pub(crate) entity_id: EntityId,
|
||||||
pub(crate) name: SharedString,
|
pub(crate) name: SharedString,
|
||||||
pub(crate) width: Option<Pixels>,
|
pub(crate) width: Pixels,
|
||||||
pub(crate) col_ix: usize,
|
pub(crate) col_ix: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,7 +83,7 @@ impl Render for DragCol {
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.shadow_md()
|
.shadow_md()
|
||||||
.when_some(self.width, |this, width| this.w(width))
|
.w(self.width)
|
||||||
.min_w(px(100.))
|
.min_w(px(100.))
|
||||||
.max_w(px(450.))
|
.max_w(px(450.))
|
||||||
.child(self.name.clone())
|
.child(self.name.clone())
|
||||||
|
|
@ -102,7 +103,7 @@ enum SelectionState {
|
||||||
pub enum TableEvent {
|
pub enum TableEvent {
|
||||||
SelectRow(usize),
|
SelectRow(usize),
|
||||||
SelectCol(usize),
|
SelectCol(usize),
|
||||||
ColWidthsChanged(Vec<Option<Pixels>>),
|
ColWidthsChanged(Vec<Pixels>),
|
||||||
MoveCol(usize, usize),
|
MoveCol(usize, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,7 +170,11 @@ pub trait TableDelegate: Sized + 'static {
|
||||||
/// Return None, use auto width.
|
/// Return None, use auto width.
|
||||||
///
|
///
|
||||||
/// This is only called when the table initializes.
|
/// This is only called when the table initializes.
|
||||||
fn col_width(&self, col_ix: usize, cx: &AppContext) -> Option<Pixels>;
|
///
|
||||||
|
/// Default: 100px
|
||||||
|
fn col_width(&self, col_ix: usize, cx: &AppContext) -> Pixels {
|
||||||
|
px(100.)
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the sort state of the column at the given index.
|
/// Return the sort state of the column at the given index.
|
||||||
///
|
///
|
||||||
|
|
@ -513,7 +518,7 @@ where
|
||||||
}
|
}
|
||||||
let size = size.floor();
|
let size = size.floor();
|
||||||
|
|
||||||
let old_width = self.col_groups[ix].width.unwrap_or_default();
|
let old_width = self.col_groups[ix].width;
|
||||||
let new_width = size;
|
let new_width = size;
|
||||||
if new_width < MIN_WIDTH {
|
if new_width < MIN_WIDTH {
|
||||||
return;
|
return;
|
||||||
|
|
@ -523,7 +528,7 @@ where
|
||||||
if changed_width > px(-1.0) && changed_width < px(1.0) {
|
if changed_width > px(-1.0) && changed_width < px(1.0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.col_groups[ix].width = Some(new_width.min(MAX_WIDTH));
|
self.col_groups[ix].width = new_width.min(MAX_WIDTH);
|
||||||
|
|
||||||
// Resize next col, table not need to resize the right cols.
|
// Resize next col, table not need to resize the right cols.
|
||||||
// let next_width = self.col_groups[ix + 1].width.unwrap_or_default();
|
// let next_width = self.col_groups[ix + 1].width.unwrap_or_default();
|
||||||
|
|
@ -619,7 +624,7 @@ where
|
||||||
let col_padding = self.col_groups[col_ix].padding;
|
let col_padding = self.col_groups[col_ix].padding;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.when_some(col_width, |this, width| this.w(width))
|
.w(col_width)
|
||||||
.h_full()
|
.h_full()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
|
@ -721,7 +726,7 @@ where
|
||||||
|
|
||||||
// sync col widths into real widths
|
// sync col widths into real widths
|
||||||
for (_, col_group) in view.col_groups.iter_mut().enumerate() {
|
for (_, col_group) in view.col_groups.iter_mut().enumerate() {
|
||||||
col_group.width = Some(col_group.bounds.size.width);
|
col_group.width = col_group.bounds.size.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ix = *ix;
|
let ix = *ix;
|
||||||
|
|
@ -970,12 +975,22 @@ where
|
||||||
let is_stripe_row = self.stripe && row_ix % 2 != 0;
|
let is_stripe_row = self.stripe && row_ix % 2 != 0;
|
||||||
let is_selected = self.selected_row == Some(row_ix);
|
let is_selected = self.selected_row == Some(row_ix);
|
||||||
let view = cx.view().clone();
|
let view = cx.view().clone();
|
||||||
|
let col_groups: Rc<Vec<ColGroup>> = Rc::new(
|
||||||
|
self.col_groups
|
||||||
|
.iter()
|
||||||
|
.skip(left_cols_count)
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
if row_ix < rows_count {
|
if row_ix < rows_count {
|
||||||
self.delegate
|
self.delegate
|
||||||
.render_tr(row_ix, cx)
|
.render_tr(row_ix, cx)
|
||||||
.context_menu(move |this, cx: &mut ViewContext<PopupMenu>| {
|
.context_menu({
|
||||||
view.read(cx).delegate.context_menu(row_ix, this, cx)
|
let view = view.clone();
|
||||||
|
move |this, cx: &mut ViewContext<PopupMenu>| {
|
||||||
|
view.read(cx).delegate.context_menu(row_ix, this, cx)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(self.size.table_row_height())
|
.h(self.size.table_row_height())
|
||||||
|
|
@ -1014,17 +1029,26 @@ where
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.h_full()
|
.h_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.children((left_cols_count..cols_count).map(|col_ix| {
|
.relative()
|
||||||
self
|
.child(table_row(
|
||||||
// Make the row scroll sync with the
|
view,
|
||||||
// horizontal_scroll_handle to support horizontal scrolling.
|
row_ix,
|
||||||
.render_col_wrap(col_ix, cx)
|
col_groups,
|
||||||
.left(horizontal_scroll_handle.offset().x)
|
self.horizontal_scroll_handle.clone(),
|
||||||
.child(
|
{
|
||||||
self.render_cell(col_ix, cx)
|
move |table, visible_range: Range<usize>, cx| {
|
||||||
.child(self.delegate.render_td(row_ix, col_ix, cx)),
|
visible_range
|
||||||
)
|
.map(|col_ix| {
|
||||||
}))
|
table.render_col_wrap(col_ix, cx).child(
|
||||||
|
table.render_cell(col_ix, cx).child(
|
||||||
|
table.delegate.render_td(row_ix, col_ix, cx),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
.child(self.delegate.render_last_empty_col(cx)),
|
.child(self.delegate.render_last_empty_col(cx)),
|
||||||
)
|
)
|
||||||
// Row selected style
|
// Row selected style
|
||||||
|
|
|
||||||
274
crates/ui/src/table_row.rs
Normal file
274
crates/ui/src/table_row.rs
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
//! Table row component for render a large number of differently sized columns (Must ensure each column width).
|
||||||
|
//!
|
||||||
|
//! Only visible columns are rendered for performance reasons.
|
||||||
|
//!
|
||||||
|
//! Inspired by uniform_list to rolate vertically to horizontally.
|
||||||
|
//!
|
||||||
|
//! https://github.com/zed-industries/zed/blob/0ae1603610ab6b265bdfbee7b8dbc23c5ab06edc/crates/gpui/src/elements/uniform_list.rs
|
||||||
|
use std::{cmp, ops::Range, rc::Rc};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
div, point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Div, Element, ElementId,
|
||||||
|
Hitbox, InteractiveElement, IntoElement, IsZero as _, Pixels, Render, ScrollHandle,
|
||||||
|
SharedString, Size, Stateful, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||||
|
};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::table::ColGroup;
|
||||||
|
|
||||||
|
pub(crate) fn table_row<R, V>(
|
||||||
|
view: View<V>,
|
||||||
|
row_ix: usize,
|
||||||
|
col_groups: Rc<Vec<ColGroup>>,
|
||||||
|
scroll_handle: ScrollHandle,
|
||||||
|
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
|
||||||
|
) -> TableRow
|
||||||
|
where
|
||||||
|
R: IntoElement,
|
||||||
|
V: Render,
|
||||||
|
{
|
||||||
|
let id = ElementId::NamedInteger(SharedString::from("table-row"), row_ix);
|
||||||
|
|
||||||
|
let render_range = move |range, cx: &mut WindowContext| {
|
||||||
|
view.update(cx, |this, cx| {
|
||||||
|
f(this, range, cx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|component| component.into_any_element())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
TableRow {
|
||||||
|
id: id.clone(),
|
||||||
|
base: div().id(id).size_full(),
|
||||||
|
scroll_handle,
|
||||||
|
cols_count: col_groups.len(),
|
||||||
|
col_groups,
|
||||||
|
render_cols: Box::new(render_range),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TableRow {
|
||||||
|
id: ElementId,
|
||||||
|
base: Stateful<Div>,
|
||||||
|
scroll_handle: ScrollHandle,
|
||||||
|
// scroll_handle: ScrollHandle,
|
||||||
|
cols_count: usize,
|
||||||
|
col_groups: Rc<Vec<ColGroup>>,
|
||||||
|
render_cols:
|
||||||
|
Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for TableRow {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
self.base.style()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frame state used by the [TableRow].
|
||||||
|
pub struct TableRowFrameState {
|
||||||
|
cols: SmallVec<[AnyElement; 32]>,
|
||||||
|
// decorations: SmallVec<[AnyElement; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableRow {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn measure_col(&self, cx: &mut WindowContext) -> Size<Pixels> {
|
||||||
|
if self.cols_count == 0 {
|
||||||
|
return Size::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let col_ix = self.cols_count - 1;
|
||||||
|
let mut items = (self.render_cols)(col_ix..col_ix + 1, cx);
|
||||||
|
let Some(mut item_to_measure) = items.pop() else {
|
||||||
|
return Size::default();
|
||||||
|
};
|
||||||
|
|
||||||
|
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||||
|
item_to_measure.layout_as_root(available_space, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for TableRow {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for TableRow {
|
||||||
|
type RequestLayoutState = TableRowFrameState;
|
||||||
|
type PrepaintState = Option<Hitbox>;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<gpui::ElementId> {
|
||||||
|
Some(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&gpui::GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||||
|
let (layout_id, _) = self.base.request_layout(global_id, cx);
|
||||||
|
|
||||||
|
(
|
||||||
|
layout_id,
|
||||||
|
TableRowFrameState {
|
||||||
|
cols: SmallVec::new(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&gpui::GlobalElementId>,
|
||||||
|
bounds: gpui::Bounds<gpui::Pixels>,
|
||||||
|
frame_state: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Self::PrepaintState {
|
||||||
|
let style = self.base.interactivity().compute_style(global_id, None, cx);
|
||||||
|
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||||
|
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||||
|
|
||||||
|
let padded_bounds = Bounds::from_corners(
|
||||||
|
bounds.origin + point(border.left + padding.left, border.top + padding.top),
|
||||||
|
bounds.lower_right()
|
||||||
|
- point(border.right + padding.right, border.bottom + padding.bottom),
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is important to get the width of each column to measure the visible columns.
|
||||||
|
//
|
||||||
|
// So the col must have a width.
|
||||||
|
let col_widths = self
|
||||||
|
.col_groups
|
||||||
|
.iter()
|
||||||
|
.map(|col| col.width.0)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let content_height = padded_bounds.size.height;
|
||||||
|
let content_width = px(col_widths.iter().sum::<f32>());
|
||||||
|
let content_size = Size {
|
||||||
|
width: content_width,
|
||||||
|
height: content_height,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.base.interactivity().prepaint(
|
||||||
|
global_id,
|
||||||
|
bounds,
|
||||||
|
content_size,
|
||||||
|
cx,
|
||||||
|
|style, _, hitbox, cx| {
|
||||||
|
let mut scroll_offset = self.scroll_handle.offset();
|
||||||
|
// dbg!(&scroll_offset);
|
||||||
|
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||||
|
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||||
|
|
||||||
|
let padded_bounds = Bounds::from_corners(
|
||||||
|
bounds.origin + point(border.left + padding.left, border.top),
|
||||||
|
bounds.lower_right() - point(border.right + padding.right, border.bottom),
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.cols_count > 0 {
|
||||||
|
let is_scrolled_horizontally = !scroll_offset.x.is_zero();
|
||||||
|
let min_horizontal_scroll_offset = padded_bounds.size.width - content_width;
|
||||||
|
if is_scrolled_horizontally && scroll_offset.x < min_horizontal_scroll_offset {
|
||||||
|
scroll_offset.x = min_horizontal_scroll_offset;
|
||||||
|
}
|
||||||
|
scroll_offset.y = Pixels::ZERO;
|
||||||
|
|
||||||
|
// Calculate the first and last visible element indices.
|
||||||
|
let mut cumulative_width = 0.0;
|
||||||
|
let mut first_visible_element_ix = 0;
|
||||||
|
for (i, &width) in col_widths.iter().enumerate() {
|
||||||
|
cumulative_width += width;
|
||||||
|
if cumulative_width > -(scroll_offset.x + padding.left).0 {
|
||||||
|
first_visible_element_ix = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cumulative_width = 0.0;
|
||||||
|
let mut last_visible_element_ix = 0;
|
||||||
|
for (i, &width) in col_widths.iter().enumerate() {
|
||||||
|
cumulative_width += width;
|
||||||
|
if cumulative_width > (-scroll_offset.x + padded_bounds.size.width).0 {
|
||||||
|
last_visible_element_ix = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last_visible_element_ix == 0 {
|
||||||
|
last_visible_element_ix = self.cols_count;
|
||||||
|
} else {
|
||||||
|
last_visible_element_ix += 1;
|
||||||
|
}
|
||||||
|
let visible_range = first_visible_element_ix
|
||||||
|
..cmp::min(last_visible_element_ix, self.cols_count);
|
||||||
|
|
||||||
|
let items = (self.render_cols)(visible_range.clone(), cx);
|
||||||
|
|
||||||
|
let content_mask = ContentMask { bounds };
|
||||||
|
cx.with_content_mask(Some(content_mask), |cx| {
|
||||||
|
for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
|
||||||
|
let item_x = px(col_widths.iter().take(ix).sum::<f32>());
|
||||||
|
|
||||||
|
let item_origin = padded_bounds.origin
|
||||||
|
+ point(item_x + scroll_offset.x + padding.left, padding.top);
|
||||||
|
// println!("{}, {}", item_origin.x, item_origin.y);
|
||||||
|
let available_height = padded_bounds.size.height;
|
||||||
|
let col_width = col_widths[ix];
|
||||||
|
let available_space = size(
|
||||||
|
AvailableSpace::Definite(px(col_width)),
|
||||||
|
AvailableSpace::Definite(available_height),
|
||||||
|
);
|
||||||
|
item.layout_as_root(available_space, cx);
|
||||||
|
item.prepaint_at(item_origin, cx);
|
||||||
|
frame_state.cols.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// let bounds = Bounds::new(
|
||||||
|
// padded_bounds.origin
|
||||||
|
// + point(scroll_offset.x + padding.left, scroll_offset.y),
|
||||||
|
// padded_bounds.size,
|
||||||
|
// );
|
||||||
|
// for decoration in &self.decorations {
|
||||||
|
// let mut decoration = decoration.as_ref().compute(
|
||||||
|
// visible_range.clone(),
|
||||||
|
// bounds,
|
||||||
|
// item_height,
|
||||||
|
// self.item_count,
|
||||||
|
// cx,
|
||||||
|
// );
|
||||||
|
// let available_space = size(
|
||||||
|
// AvailableSpace::Definite(bounds.size.width),
|
||||||
|
// AvailableSpace::Definite(bounds.size.height),
|
||||||
|
// );
|
||||||
|
// decoration.layout_as_root(available_space, cx);
|
||||||
|
// decoration.prepaint_at(bounds.origin, cx);
|
||||||
|
// frame_state.decorations.push(decoration);
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hitbox
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&gpui::GlobalElementId>,
|
||||||
|
bounds: gpui::Bounds<gpui::Pixels>,
|
||||||
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
|
hitbox: &mut Self::PrepaintState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
self.base
|
||||||
|
.interactivity()
|
||||||
|
.paint(global_id, bounds, hitbox.as_ref(), cx, |_, cx| {
|
||||||
|
for col in &mut request_layout.cols {
|
||||||
|
col.paint(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue