Skip to content

Commit 7d4b669

Browse files
committed
feat: table
1 parent efe23b0 commit 7d4b669

File tree

1 file changed

+184
-51
lines changed

1 file changed

+184
-51
lines changed

src/main.rs

Lines changed: 184 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ use settings::Settings;
1010
use wavelog::send;
1111
use udp::UdpListener;
1212

13-
use iced::widget::{Column, Container, Text, Scrollable};
13+
use iced::widget::{Column, Container, Text, Scrollable, Row, Space, Tooltip};
1414
use iced::{Color, Element, Length, Task};
1515

16-
const MAX_LOG_LINES: usize = 50;
16+
const MAX_LOG_LINES: usize = 500;
1717

1818
#[derive(Debug)]
1919
struct RustWavelogGateApp {
20-
lines: Vec<String>,
20+
qso_records: Vec<(QSO, String)>, // QSO records and status
21+
status_message: String, // Status bar info
22+
listen_info: String, // Listen info
2123
settings: Option<Settings>,
2224
}
2325

@@ -31,7 +33,9 @@ enum Message {
3133
impl RustWavelogGateApp {
3234
pub fn new() -> (Self, Task<Message>) {
3335
let app = Self {
34-
lines: vec!["Loading...".to_string()],
36+
qso_records: Vec::new(),
37+
status_message: "Loading...".to_string(),
38+
listen_info: String::new(),
3539
settings: None,
3640
};
3741

@@ -48,24 +52,18 @@ impl RustWavelogGateApp {
4852
listener.listen_once().await.unwrap_or_default()
4953
}
5054

51-
fn add_log_line(&mut self, line: String) {
52-
self.lines.push(line);
53-
if self.lines.len() > MAX_LOG_LINES {
54-
self.lines.remove(0);
55+
fn add_qso_record(&mut self, qso: QSO, status: String) {
56+
self.qso_records.insert(0, (qso, status)); // 插入到开头,新QSO在顶部
57+
if self.qso_records.len() > MAX_LOG_LINES {
58+
self.qso_records.pop(); // 删除最后一个(最旧的)记录
5559
}
5660
}
5761

58-
fn format_qso_log(&self, qso: &QSO, status: &str) -> String {
59-
if !qso.gridsquare.is_empty() {
60-
format!(
61-
"{} {} ({}) on {} (R:{} / S:{}) - {}",
62-
qso.time_on, qso.call, qso.gridsquare, qso.band, qso.rst_sent, qso.rst_rcvd, status
63-
)
62+
fn get_status_display(status: &str) -> &str {
63+
if status == "OK" {
64+
"OK"
6465
} else {
65-
format!(
66-
"{} {} on {} (R:{} / S:{}) - {}",
67-
qso.time_on, qso.call, qso.band, qso.rst_sent, qso.rst_rcvd, status
68-
)
66+
"Error"
6967
}
7068
}
7169

@@ -100,7 +98,7 @@ impl RustWavelogGateApp {
10098
async move {
10199
let status = match send(&qso, &settings).await {
102100
Ok(_) => "OK".to_string(),
103-
Err(e) => format!("ERROR {}", e),
101+
Err(e) => format!("{}", e),
104102
};
105103
(qso, status)
106104
},
@@ -126,17 +124,17 @@ impl RustWavelogGateApp {
126124
fn handle_settings_loaded(&mut self, result: Result<Settings, String>) -> Task<Message> {
127125
match result {
128126
Ok(settings) => {
129-
self.lines.clear();
130-
self.lines.push(format!("Listen: {}:{}", settings.server.host, settings.server.port));
131-
self.lines.push(format!("Wavelog Server: {}", settings.wavelog.url));
127+
self.listen_info = format!("Listen: {}:{} | Wavelog: {}",
128+
settings.server.host, settings.server.port, settings.wavelog.url);
129+
self.status_message = "Ready".to_string();
132130

133131
let task = self.restart_udp_listener(&settings);
134132
self.settings = Some(settings);
135133
task
136134
}
137135
Err(e) => {
138-
self.lines.clear();
139-
self.lines.push(format!("Error: {}", e));
136+
self.status_message = format!("Config load failed: {}", e);
137+
self.listen_info = String::new();
140138
Task::none()
141139
}
142140
}
@@ -151,42 +149,175 @@ impl RustWavelogGateApp {
151149

152150
fn handle_qso_received(&mut self, qso: QSO, status: String) -> Task<Message> {
153151
if !qso.call.is_empty() {
154-
let log_line = self.format_qso_log(&qso, &status);
155-
self.add_log_line(log_line);
152+
self.add_qso_record(qso, status);
153+
self.status_message = "QSO processed".to_string();
156154
}
157155
Task::none()
158156
}
159157

160158
pub fn view(&self) -> Element<Message> {
161-
let content = Column::with_children(
162-
self.lines
163-
.iter()
164-
.map(|line| {
165-
Text::new(line)
166-
.size(14)
167-
.line_height(1.5)
168-
.color(Color::WHITE)
169-
.font(iced::Font::MONOSPACE)
170-
.into()
159+
// Create sticky table header
160+
let sticky_header = Container::new(
161+
Row::new()
162+
.push(Text::new("Time").width(Length::Fixed(60.0)).size(14).color(Color::from_rgb(0.8, 0.8, 0.8)))
163+
.push(Text::new("Call").width(Length::Fixed(120.0)).size(14).color(Color::from_rgb(0.8, 0.8, 0.8)))
164+
.push(Text::new("Grid").width(Length::Fixed(60.0)).size(14).color(Color::from_rgb(0.8, 0.8, 0.8)))
165+
.push(Text::new("Band").width(Length::Fixed(50.0)).size(14).color(Color::from_rgb(0.8, 0.8, 0.8)))
166+
.push(Text::new("Mode").width(Length::Fixed(50.0)).size(14).color(Color::from_rgb(0.8, 0.8, 0.8)))
167+
.push(Text::new("RST").width(Length::Fixed(64.0)).size(14).color(Color::from_rgb(0.8, 0.8, 0.8)))
168+
.push(Text::new("Status").width(Length::Fixed(50.0)).size(14).color(Color::from_rgb(0.8, 0.8, 0.8)))
169+
.padding(10)
170+
.spacing(5)
171+
)
172+
.style(|_| {
173+
iced::widget::container::Style {
174+
background: Some(iced::Background::Color(Color::from_rgb(0.15, 0.15, 0.15))),
175+
border: iced::Border {
176+
color: Color::from_rgb(0.4, 0.4, 0.4),
177+
width: 1.0,
178+
radius: 0.0.into(),
179+
},
180+
..Default::default()
181+
}
182+
})
183+
.width(Length::Fill);
184+
185+
// Create table data rows only
186+
let mut data_rows = Vec::new();
187+
188+
for (qso, status) in &self.qso_records {
189+
let status_color = if status == "OK" {
190+
Color::from_rgb(0.0, 0.8, 0.0)
191+
} else {
192+
Color::from_rgb(0.8, 0.0, 0.0)
193+
};
194+
195+
let status_display = Self::get_status_display(status);
196+
197+
let status_element: Element<Message> = if status == "OK" {
198+
Text::new(status_display).width(Length::Fixed(100.0)).size(12).color(status_color).font(iced::Font::MONOSPACE).into()
199+
} else {
200+
Tooltip::new(
201+
Text::new(status_display).width(Length::Fixed(100.0)).size(12).color(status_color).font(iced::Font::MONOSPACE),
202+
Text::new(status.as_str()).size(11),
203+
iced::widget::tooltip::Position::Top
204+
)
205+
.style(|_theme| {
206+
iced::widget::container::Style {
207+
background: Some(iced::Background::Color(Color::from_rgba(0.0, 0.0, 0.0, 0.9))),
208+
border: iced::Border {
209+
color: Color::from_rgb(0.5, 0.5, 0.5),
210+
width: 1.0,
211+
radius: 4.0.into(),
212+
},
213+
text_color: Some(Color::WHITE),
214+
shadow: iced::Shadow {
215+
color: Color::from_rgba(0.0, 0.0, 0.0, 0.5),
216+
offset: iced::Vector::new(0.0, 2.0),
217+
blur_radius: 4.0,
218+
},
219+
}
171220
})
172-
.collect::<Vec<_>>(),
221+
.into()
222+
};
223+
224+
let row = Container::new(
225+
Row::new()
226+
.push(Text::new(&qso.time_on).width(Length::Fixed(60.0)).size(12).color(Color::WHITE).font(iced::Font::MONOSPACE))
227+
.push(Text::new(&qso.call).width(Length::Fixed(120.0)).size(12).color(Color::WHITE).font(iced::Font::MONOSPACE))
228+
.push(Text::new(&qso.gridsquare).width(Length::Fixed(60.0)).size(12).color(Color::WHITE).font(iced::Font::MONOSPACE))
229+
.push(Text::new(&qso.band).width(Length::Fixed(50.0)).size(12).color(Color::WHITE).font(iced::Font::MONOSPACE))
230+
.push(Text::new(&qso.mode).width(Length::Fixed(50.0)).size(12).color(Color::WHITE).font(iced::Font::MONOSPACE))
231+
.push(Text::new(format!("{}/{}", qso.rst_sent, qso.rst_rcvd)).width(Length::Fixed(64.0)).size(12).color(Color::WHITE).font(iced::Font::MONOSPACE))
232+
.push(status_element)
233+
.padding(10)
234+
.spacing(5)
235+
)
236+
.style(|_| {
237+
iced::widget::container::Style {
238+
background: Some(iced::Background::Color(Color::from_rgb(0.12, 0.12, 0.12))),
239+
border: iced::Border {
240+
color: Color::from_rgb(0.3, 0.3, 0.3),
241+
width: 1.0,
242+
radius: 0.0.into(),
243+
},
244+
..Default::default()
245+
}
246+
})
247+
.width(Length::Fill);
248+
249+
data_rows.push(row.into());
250+
}
251+
252+
// Create scrollable content for data rows only
253+
let scrollable_content: Element<Message> = if self.qso_records.is_empty() {
254+
// When empty, show centered message
255+
Container::new(
256+
Text::new("No QSO records").size(14).color(Color::from_rgb(0.6, 0.6, 0.6))
257+
)
258+
.center_x(Length::Fill)
259+
.center_y(Length::Fill)
260+
.width(Length::Fill)
261+
.height(Length::Fill)
262+
.style(|_| {
263+
iced::widget::container::Style {
264+
background: Some(iced::Background::Color(Color::from_rgb(0.12, 0.12, 0.12))),
265+
border: iced::Border {
266+
color: Color::from_rgb(0.3, 0.3, 0.3),
267+
width: 1.0,
268+
radius: 0.0.into(),
269+
},
270+
..Default::default()
271+
}
272+
})
273+
.into()
274+
} else {
275+
// When has data, show scrollable list
276+
Scrollable::new(
277+
Column::with_children(data_rows)
278+
.width(Length::Fill)
279+
)
280+
.width(Length::Fill)
281+
.height(Length::Fill)
282+
.into()
283+
};
284+
285+
// Create status bar with border
286+
let status_bar = Container::new(
287+
Row::new()
288+
.push(Text::new(&self.listen_info).size(12).color(Color::from_rgb(0.7, 0.7, 0.7)))
289+
.push(Space::with_width(Length::Fill))
290+
.push(Text::new(&self.status_message).size(12).color(Color::from_rgb(0.9, 0.9, 0.9)))
291+
.padding(8)
292+
.width(Length::Fill)
173293
)
174-
.padding(iced::Padding {
175-
top: 10.0,
176-
bottom: 10.0,
177-
left: 10.0,
178-
right: 10.0,
294+
.style(|_| {
295+
iced::widget::container::Style {
296+
background: Some(iced::Background::Color(Color::from_rgb(0.08, 0.08, 0.08))),
297+
border: iced::Border {
298+
color: Color::from_rgb(0.4, 0.4, 0.4),
299+
width: 1.0,
300+
radius: 0.0.into(),
301+
},
302+
..Default::default()
303+
}
179304
})
180305
.width(Length::Fill);
181306

182-
let scrollable = Scrollable::new(content).anchor_bottom();
307+
// Main layout: sticky header + scrollable data + status bar
308+
let main_content = Column::new()
309+
.push(sticky_header)
310+
.push(scrollable_content)
311+
.push(status_bar)
312+
.width(Length::Fill)
313+
.height(Length::Fill);
183314

184-
Container::new(scrollable)
315+
Container::new(main_content)
185316
.width(Length::Fill)
186317
.height(Length::Fill)
187318
.style(|_| {
188319
iced::widget::container::Style {
189-
background: Some(iced::Background::Color(Color::BLACK)),
320+
background: Some(iced::Background::Color(Color::from_rgb(0.1, 0.1, 0.1))),
190321
..Default::default()
191322
}
192323
})
@@ -197,23 +328,25 @@ impl RustWavelogGateApp {
197328
impl Default for RustWavelogGateApp {
198329
fn default() -> Self {
199330
Self {
200-
lines: vec!["Starting...".to_string()],
331+
qso_records: Vec::new(),
332+
status_message: "Starting...".to_string(),
333+
listen_info: String::new(),
201334
settings: None,
202335
}
203336
}
204337
}
205338

206339
fn main() -> iced::Result {
207-
iced::application("Rust Wavelog Gate", RustWavelogGateApp::update, RustWavelogGateApp::view)
340+
iced::application("Wavelog Gate", RustWavelogGateApp::update, RustWavelogGateApp::view)
208341
.theme(|_state| iced::Theme::Dark)
209342
.window(iced::window::Settings {
210343
size: iced::Size {
211-
width: 600.0,
212-
height: 200.0,
344+
width: 520.0,
345+
height: 240.0,
213346
},
214347
min_size: Some(iced::Size {
215-
width: 400.0,
216-
height: 150.0,
348+
width: 520.0,
349+
height: 240.0,
217350
}),
218351
resizable: true,
219352
decorations: true,

0 commit comments

Comments
 (0)