1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::models::{GridColumnDef, GridLabels, GridOptions};
6
7pub type PinnedColumnState = BTreeMap<String, String>;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum PinDirection {
12 Left,
13 Right,
14 None,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct PinnedOffset {
20 pub side: String,
21 pub offset: String,
22}
23
24pub fn is_pinning_enabled(options: &GridOptions) -> bool {
25 options.enable_pinning
26}
27
28pub fn is_column_pinnable(options: &GridOptions, column: &GridColumnDef) -> bool {
29 is_pinning_enabled(options) && column.enable_pinning
30}
31
32pub fn get_column_pin_direction(
33 pinned_columns: &PinnedColumnState,
34 column: &GridColumnDef,
35) -> PinDirection {
36 match pinned_columns.get(&column.name).map(String::as_str) {
37 Some("left") => PinDirection::Left,
38 Some("right") => PinDirection::Right,
39 _ => PinDirection::None,
40 }
41}
42
43pub fn pin_column_state(
44 current: &PinnedColumnState,
45 column_name: &str,
46 direction: PinDirection,
47) -> PinnedColumnState {
48 let mut next = current.clone();
49 match direction {
50 PinDirection::None => {
51 next.remove(column_name);
52 }
53 PinDirection::Left => {
54 next.insert(column_name.to_string(), "left".to_string());
55 }
56 PinDirection::Right => {
57 next.insert(column_name.to_string(), "right".to_string());
58 }
59 }
60 next
61}
62
63pub fn build_initial_pinned_state(columns: &[GridColumnDef]) -> PinnedColumnState {
64 let mut state = PinnedColumnState::new();
65 for column in columns {
66 if column.pinned_left {
67 state.insert(column.name.clone(), "left".to_string());
68 } else if column.pinned_right {
69 state.insert(column.name.clone(), "right".to_string());
70 }
71 }
72 state
73}
74
75pub fn compute_pinned_offset(
76 visible_columns: &[GridColumnDef],
77 pinned_columns: &PinnedColumnState,
78 column: &GridColumnDef,
79) -> Option<PinnedOffset> {
80 let direction = pinned_columns.get(&column.name)?;
81
82 fn resolve_column_width_for_offset(column: &GridColumnDef) -> String {
83 let Some(width) = &column.width else {
84 return "11rem".to_string();
85 };
86
87 if width.contains("fr") || width.contains("minmax") {
88 return "11rem".to_string();
89 }
90
91 width.clone()
92 }
93
94 if direction == "left" {
95 let mut offset_parts = Vec::new();
96 for current in visible_columns {
97 if current.name == column.name {
98 break;
99 }
100 if pinned_columns
101 .get(¤t.name)
102 .is_some_and(|side| side == "left")
103 {
104 offset_parts.push(resolve_column_width_for_offset(current));
105 }
106 }
107
108 return Some(PinnedOffset {
109 side: "left".to_string(),
110 offset: if offset_parts.is_empty() {
111 "0px".to_string()
112 } else {
113 format!("calc({})", offset_parts.join(" + "))
114 },
115 });
116 }
117
118 if direction == "right" {
119 let mut offset_parts = Vec::new();
120 for current in visible_columns.iter().rev() {
121 if current.name == column.name {
122 break;
123 }
124 if pinned_columns
125 .get(¤t.name)
126 .is_some_and(|side| side == "right")
127 {
128 offset_parts.push(resolve_column_width_for_offset(current));
129 }
130 }
131
132 return Some(PinnedOffset {
133 side: "right".to_string(),
134 offset: if offset_parts.is_empty() {
135 "0px".to_string()
136 } else {
137 format!("calc({})", offset_parts.join(" + "))
138 },
139 });
140 }
141
142 None
143}
144
145pub fn pinning_button_label(
146 pinned_columns: &PinnedColumnState,
147 column: &GridColumnDef,
148 labels: &GridLabels,
149) -> String {
150 match pinned_columns.get(&column.name).map(String::as_str) {
151 Some("left") | Some("right") => labels.unpin.clone(),
152 _ => labels.pin_left.clone(),
153 }
154}