1use gpui_kit::{
16 App, Bounds, Context, FontWeight, Hsla, Pixels, Point, SharedString, Window, point, px, size,
17};
18use rust_i18n::t;
19
20use super::{
21 CellChange, CellStyle, ColumnAlign, ColumnOf, KeyOf, LiveGridDelegate, LiveGridState,
22 ViewColumnIx,
23 a11y::{PaintedRow, RowOutline},
24 change::ChangePainter as _,
25 columns::column_of,
26 frame::{CellText, CellTextContext, RowPlanner, place_cell_text},
27 menu::GridTarget,
28 pinned::BodyLine,
29 style::{AggregateLine, AggregateStyleContext},
30 tooltip::{CellTooltip, HoveredCell},
31};
32use crate::{
33 element::Region,
34 engine::{AggregateRow, GroupPath},
35 highlight::Stamp,
36 model::{ColumnIx, GridColumns as _, NumberFormat, OwnedValue, display_text_in},
37 store::PinnedEdge,
38 text::{GlyphArena, TextCaches},
39};
40
41pub(super) const TOTALS_WEIGHT: FontWeight = FontWeight::SEMIBOLD;
44
45#[derive(Debug, Default)]
48pub(crate) struct SummaryTexts {
49 locale: String,
51 total: SharedString,
52}
53
54impl SummaryTexts {
55 pub(super) fn refresh(&mut self) {
57 let locale = rust_i18n::locale();
58 if *locale == *self.locale {
59 return;
60 }
61 self.total = t!("live_grid.totals.label", locale = &*locale)
62 .into_owned()
63 .into();
64 self.locale = locale.to_string();
65 }
66
67 pub(super) fn total(&self) -> &str {
69 &self.total
70 }
71}
72
73impl<D: LiveGridDelegate> LiveGridState<D> {
74 pub fn with_totals(mut self, edge: PinnedEdge) -> Self {
79 self.totals = Some(edge);
80 self
81 }
82
83 pub fn set_totals(&mut self, edge: Option<PinnedEdge>, cx: &mut Context<Self>) {
86 if self.totals == edge {
87 return;
88 }
89 self.totals = edge;
90 self.clamp_scroll();
92 cx.notify();
93 }
94
95 pub fn totals(&self) -> Option<PinnedEdge> {
97 self.totals
98 }
99
100 pub(super) fn summary_target(
105 &self,
106 position: Point<Pixels>,
107 ) -> Option<GridTarget<KeyOf<D>, ColumnOf<D>>> {
108 let column = || {
109 self.column_at_position(position)
110 .or_else(|| {
111 if position.x < self.body_bounds.origin.x {
112 self.columns.visible().next()
113 } else {
114 self.columns.visible().last()
115 }
116 })
117 .map(column_of)
118 };
119 match self.body_line_at(position)? {
120 BodyLine::Totals(edge) => Some(GridTarget::Totals {
121 edge,
122 column: column()?,
123 }),
124 BodyLine::Data(line) => Some(GridTarget::GroupFooter {
125 group: self.painted.get(line)?.footer()?.clone(),
126 column: column()?,
127 }),
128 BodyLine::Pinned(..) => None,
129 }
130 }
131
132 pub(super) fn is_footer_shown(&self, path: &GroupPath) -> bool {
136 let projection = &self.projection;
137 projection.groups().find(path).is_some_and(|group| {
138 group.has_footer() && group.is_expanded() && projection.group_line(group.id()).is_some()
139 })
140 }
141}
142
143pub(super) struct PlannedSummary<'a> {
145 pub(super) line: AggregateLine<'a>,
146 pub(super) aggregates: AggregateRow<'a>,
147 pub(super) stamps: Option<&'a [Stamp]>,
149 pub(super) y: Pixels,
151 pub(super) background: Option<Hsla>,
154 pub(super) weight: Option<FontWeight>,
156 pub(super) label: Option<&'a str>,
159 pub(super) hovered_column: Option<ColumnIx>,
160 pub(super) row: PaintedRow,
162 pub(super) outline: RowOutline,
163}
164
165impl<D: LiveGridDelegate> RowPlanner<'_, D> {
166 pub(super) fn plan_summary(
174 &mut self,
175 summary: &PlannedSummary<'_>,
176 regions: &mut [Region; 3],
177 window: &mut Window,
178 ) {
179 let (y, row_height, palette) = (summary.y, self.row_height, self.palette);
180 let key_hash = summary.row.key_hash;
181 if let Some(a11y) = &mut self.a11y {
182 a11y.push_outlined_row(summary.row, summary.outline);
183 if let Some(label) = summary.label {
184 a11y.set_row_label(label);
185 }
186 }
187 for (slot, region) in regions.iter_mut().enumerate() {
188 let row_x = region.mask().origin.x;
189 let row_width = self.row_extents[slot];
190 if let Some(background) = summary.background {
191 region.quads.push((
192 Bounds::new(point(row_x, y), size(row_width, row_height)),
193 background,
194 ));
195 }
196 for column_slot in self.zone_ranges[slot].clone() {
197 let planned = &self.planned[column_slot];
198 let column = planned.column;
199 let (x, width) = (planned.x, planned.width);
200 let owned = summary.aggregates.get(column.ix());
201 let value = owned.map(OwnedValue::as_cell);
202 let label = summary
203 .label
204 .filter(|_| owned.is_none() && planned.view == ViewColumnIx::new(0));
205 let change = owned
206 .and(summary.stamps)
207 .and_then(|stamps| stamps.get(column.ix().get()))
208 .and_then(|stamp| {
209 self.policy
210 .intensity(*stamp, self.tick)
211 .map(|intensity| CellChange {
212 intensity,
213 change: stamp.change(),
214 direction: stamp.direction(),
215 column_ix: Some(column.ix()),
216 })
217 });
218 self.any_hot |= change.is_some();
219
220 let mut style = CellStyle::default();
221 if let Some(weight) = summary.weight {
222 style.set_font_weight(weight);
223 }
224 if let Some(change) = &change {
225 self.change_paint
226 .paint(change, &self.change_palette, &mut style);
227 }
228 self.delegate.aggregate_style(
229 &AggregateStyleContext {
230 line: summary.line,
231 column,
232 value,
233 change,
234 focused: false,
235 },
236 &mut style,
237 self.app,
238 );
239 let cell_bounds = Bounds::new(point(x, y), size(width, row_height));
240 if let Some(background) = style.background {
241 region.quads.push((cell_bounds, background));
242 }
243 let right = match planned.align {
244 ColumnAlign::End => true,
245 ColumnAlign::Start => false,
246 ColumnAlign::Auto => value.is_some_and(|value| value.is_number()),
247 };
248 let text = match (value, label) {
249 (Some(value), _) => {
250 display_text_in(value, planned.number_format, &mut self.buffer)
251 }
252 (None, Some(label)) => label,
253 (None, None) => "",
254 };
255 let (truncated, icons) = place_cell_text(
256 CellText {
257 text,
258 x,
259 width,
260 y,
261 right,
262 lead: None,
263 trend: style.trend,
264 weight: style.font_weight,
265 color: style.foreground.unwrap_or(palette.pinned_foreground),
266 },
267 &mut CellTextContext {
268 text_cache: &mut *self.text_cache,
269 glyphs: &mut self.glyphs,
270 arrows: &self.arrows,
271 row_height,
272 },
273 region,
274 window,
275 );
276 self.icons += icons;
277 self.cells += 1;
278 let name = owned.map(|_| {
280 self.captions
281 .aggregates
282 .iter()
283 .find(|(aggregated, _)| *aggregated == column)
284 .map_or("", |(_, aggregate)| {
285 self.captions.texts.aggregate_name(aggregate)
286 })
287 });
288 if let Some(a11y) = &mut self.a11y {
289 match name {
290 Some(name) => {
291 a11y.push_aggregate_cell(column_slot, text, name);
292 if let Some(trend) = style.trend {
293 a11y.set_trend(trend);
294 }
295 }
296 None if label.is_some() => a11y.push_cell(column_slot, text, false),
297 None => {}
298 }
299 }
300 if summary.hovered_column == Some(column.ix()) {
301 let tooltip = match name {
305 Some(name) => Some(CellTooltip::text(
306 self.captions.texts.aggregate_tooltip(name, text),
307 )),
308 None if truncated => {
309 Some(CellTooltip::text(SharedString::from(text.to_owned())))
310 }
311 None => None,
312 };
313 self.hovered = Some(HoveredCell {
314 key_hash,
315 column: column.ix(),
316 id: column.id(),
317 bounds: cell_bounds.intersect(®ion.mask()),
318 tooltip,
319 });
320 }
321 }
322 region.lines.push((
323 Bounds::new(
324 point(row_x, y + row_height - px(1.)),
325 size(row_width, px(1.)),
326 ),
327 palette.row_border,
328 ));
329 }
330 }
331}
332
333pub(super) struct SummaryLine<'a> {
336 pub(super) line: AggregateLine<'a>,
337 pub(super) aggregates: AggregateRow<'a>,
338 pub(super) weight: Option<FontWeight>,
340 pub(super) label: Option<(&'a str, ColumnIx)>,
342}
343
344pub(super) struct SummaryMeasure<'a, D: LiveGridDelegate> {
346 pub(super) delegate: &'a D,
347 pub(super) app: &'a App,
348 pub(super) text_cache: &'a mut TextCaches,
349 pub(super) buffer: &'a mut String,
350 pub(super) arena: &'a mut GlyphArena,
351 pub(super) format: NumberFormat,
353}
354
355impl<D: LiveGridDelegate> SummaryMeasure<'_, D> {
356 pub(super) fn width(
361 &mut self,
362 lines: &[SummaryLine<'_>],
363 column: ColumnOf<D>,
364 window: &Window,
365 ) -> Pixels {
366 let mut widest = Pixels::ZERO;
367 for line in lines {
368 let owned = line.aggregates.get(column.ix());
369 let label = line
370 .label
371 .filter(|(_, label_column)| owned.is_none() && *label_column == column.ix())
372 .map(|(label, _)| label);
373 if owned.is_none() && label.is_none() {
374 continue;
375 }
376 let value = owned.map(OwnedValue::as_cell);
377 let mut style = CellStyle::default();
378 if let Some(weight) = line.weight {
379 style.set_font_weight(weight);
380 }
381 self.delegate.aggregate_style(
382 &AggregateStyleContext {
383 line: line.line,
384 column,
385 value,
386 change: None,
387 focused: false,
388 },
389 &mut style,
390 self.app,
391 );
392 let text = match (value, label) {
393 (Some(value), _) => display_text_in(value, self.format, self.buffer),
394 (None, Some(label)) => label,
395 (None, None) => continue,
396 };
397 if text.is_empty() {
398 continue;
399 }
400 self.arena.clear();
401 let width = self
402 .text_cache
403 .for_weight(style.font_weight)
404 .layout(text, window, self.arena)
405 .width();
406 widest = widest.max(width);
407 }
408 widest
409 }
410}