1use serde::{Deserialize, Serialize};
2
3use super::{Element, LayoutMode, RenderContext, RenderResult};
4use crate::{
5 layout::{FixedBox, OverflowPolicy},
6 styles::RgbColor,
7};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FieldRect {
14 pub x_mm: f64,
15 pub y_mm: f64,
16 pub width_mm: f64,
17 pub height_mm: f64,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct TextFieldDef {
25 pub name: String,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub default_value: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub tooltip: Option<String>,
30 pub multiline: bool,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub max_length: Option<u32>,
33 pub readonly: bool,
34 pub required: bool,
35 pub rect: FieldRect,
36 pub font_size: f64,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct CheckBoxDef {
42 pub name: String,
43 pub checked_by_default: bool,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub tooltip: Option<String>,
46 pub rect: FieldRect,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct RadioButtonDef {
52 pub group_name: String,
54 pub value: String,
56 pub selected_by_default: bool,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub tooltip: Option<String>,
59 pub rect: FieldRect,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ComboBoxDef {
65 pub name: String,
66 pub options: Vec<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub default_value: Option<String>,
69 pub editable: bool,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub tooltip: Option<String>,
72 pub rect: FieldRect,
73 pub font_size: f64,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ListBoxDef {
79 pub name: String,
80 pub options: Vec<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub default_value: Option<String>,
83 pub multi_select: bool,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub tooltip: Option<String>,
86 pub rect: FieldRect,
87 pub font_size: f64,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(tag = "field_type", rename_all = "snake_case")]
98pub enum FormField {
99 TextField(TextFieldDef),
100 CheckBox(CheckBoxDef),
101 RadioButton(RadioButtonDef),
102 ComboBox(ComboBoxDef),
103 ListBox(ListBoxDef),
104}
105
106impl FormField {
107 fn rect(&self) -> &FieldRect {
108 match self {
109 FormField::TextField(d) => &d.rect,
110 FormField::CheckBox(d) => &d.rect,
111 FormField::RadioButton(d) => &d.rect,
112 FormField::ComboBox(d) => &d.rect,
113 FormField::ListBox(d) => &d.rect,
114 }
115 }
116
117 fn name(&self) -> &str {
118 match self {
119 FormField::TextField(d) => &d.name,
120 FormField::CheckBox(d) => &d.name,
121 FormField::RadioButton(d) => &d.group_name,
122 FormField::ComboBox(d) => &d.name,
123 FormField::ListBox(d) => &d.name,
124 }
125 }
126}
127
128impl Element for FormField {
129 fn layout_mode(&self) -> LayoutMode {
130 let r = self.rect();
131 LayoutMode::Fixed(FixedBox {
132 x_mm: r.x_mm,
133 y_mm: r.y_mm,
134 width_mm: r.width_mm,
135 height_mm: r.height_mm,
136 z_index: 0,
137 overflow: OverflowPolicy::Clip,
138 border: None,
139 background: None,
140 padding_mm: 0.0,
141 ua_role: None,
142 ua_alt: None,
143 })
144 }
145
146 fn estimated_height_mm(&self) -> f64 {
147 0.0
148 }
149
150 fn render(&self, ctx: &mut RenderContext) -> crate::Result<RenderResult> {
151 let r = self.rect();
152 let name = self.name().to_string();
153
154 let (fill_r, fill_g, fill_b) = match self {
155 FormField::TextField(_) | FormField::ComboBox(_) | FormField::ListBox(_) =>
156 (0.93_f64, 0.96_f64, 1.0_f64),
157 FormField::CheckBox(_) | FormField::RadioButton(_) =>
158 (1.0_f64, 1.0_f64, 1.0_f64),
159 };
160
161 let fill = RgbColor { r: fill_r, g: fill_g, b: fill_b };
162 let stroke = RgbColor { r: 0.4, g: 0.4, b: 0.8 };
163 ctx.backend.draw_rect_stroked(
164 r.x_mm, r.y_mm, r.width_mm, r.height_mm,
165 &fill, &stroke, 0.5,
166 )?;
167
168 match self {
169 FormField::CheckBox(d) if d.checked_by_default => {
170 render_checkmark(ctx, r)?;
171 }
172 FormField::RadioButton(d) if d.selected_by_default => {
173 render_radio_dot(ctx, r)?;
174 }
175 _ => {}
176 }
177
178 if let Some(font_ref) = ctx.get_font_ref(false, false) {
180 let label_fs = 7.0_f64;
181 let label_color = RgbColor { r: 0.3, g: 0.3, b: 0.6 };
182 let text_x = r.x_mm + 1.5 * 25.4 / 72.0;
183 let text_y = r.y_mm + r.height_mm / 2.0 - label_fs * 25.4 / 72.0 / 2.0;
184 ctx.draw_text(&name, text_x, text_y, label_fs, font_ref, &label_color)?;
185 }
186
187 Ok(RenderResult::done())
188 }
189}
190
191fn render_checkmark(ctx: &mut RenderContext, r: &FieldRect) -> crate::Result<()> {
192 let cx = r.x_mm + r.width_mm * 0.25;
193 let cy = r.y_mm + r.height_mm * 0.5;
194 let cr = r.width_mm.min(r.height_mm) * 0.3;
195 let color = RgbColor { r: 0.0, g: 0.5, b: 0.0 };
196 let width_pt = 1.0_f32;
197 ctx.backend.draw_line(cx - cr * 0.3, cy, cx, cy - cr * 0.5, width_pt, &color)?;
199 ctx.backend.draw_line(cx, cy - cr * 0.5, cx + cr, cy + cr * 0.8, width_pt, &color)?;
200 Ok(())
201}
202
203fn render_radio_dot(ctx: &mut RenderContext, r: &FieldRect) -> crate::Result<()> {
204 let dot_size = r.width_mm.min(r.height_mm) * 0.4;
205 let dx = r.x_mm + r.width_mm / 2.0 - r.width_mm * 0.2;
206 let dy = r.y_mm + r.height_mm / 2.0 - r.height_mm * 0.2;
207 let color = RgbColor { r: 0.2, g: 0.2, b: 0.8 };
208 ctx.backend.draw_rect(dx, dy, dot_size, dot_size, &color)?;
209 Ok(())
210}