@@ -10,14 +10,16 @@ use settings::Settings;
1010use wavelog:: send;
1111use udp:: UdpListener ;
1212
13- use iced:: widget:: { Column , Container , Text , Scrollable } ;
13+ use iced:: widget:: { Column , Container , Text , Scrollable , Row , Space , Tooltip } ;
1414use iced:: { Color , Element , Length , Task } ;
1515
16- const MAX_LOG_LINES : usize = 50 ;
16+ const MAX_LOG_LINES : usize = 500 ;
1717
1818#[ derive( Debug ) ]
1919struct 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 {
3133impl 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 {
197328impl 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
206339fn 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