1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
use crate::app::BywindApp;
impl BywindApp {
/// Top menu bar: File menu (load / save wind map and solution, quit) plus
/// the egui theme toggle. The native-only file-dialog branches are gated
/// with `cfg(not(target_arch = "wasm32"))`.
pub(crate) fn render_menu_bar(&mut self, ui: &mut egui::Ui) {
egui::Panel::top("top_panel").show_inside(ui, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
let is_web = cfg!(target_arch = "wasm32");
if !is_web {
ui.menu_button("File", |ui| {
// Same reason as the Advanced menu below: egui's
// automatic menu sizing clips long entries on
// some font/zoom combos. Pin a width that holds
// the longest label ("Load Scenario (TOML)...").
ui.set_min_width(200.0);
#[cfg(not(target_arch = "wasm32"))]
{
if ui.button("Load GRIB2...").clicked() {
// Defer to the in-app dialog so stride
// and bbox can be reviewed before the
// native file picker pops up.
ui.close();
self.editor.grib2_load_dialog_open = true;
}
let has_map = self.wind_map.is_some();
// AV1 near-lossless `.wcav` is the only
// binary wind-map format now (wc1 was
// retired in Phase 1.4). Encode happens on
// the UI thread; rav1e at our settings is
// ~30 s for a 720-frame map.
if ui
.add_enabled(has_map, egui::Button::new("Save Wind Map..."))
.on_disabled_hover_text("Load or generate a wind map first.")
.clicked()
{
ui.close();
if let Some(path) = rfd::FileDialog::new()
.add_filter("Bywind AV1", &["wcav"])
.set_file_name("wind_map.wcav")
.save_file()
{
self.save_windmap_av1_to_file(&path);
}
}
if ui.button("Load Wind Map...").clicked() {
ui.close();
if let Some(path) = rfd::FileDialog::new()
.add_filter("Bywind AV1", &["wcav"])
.pick_file()
{
self.load_windmap_av1_from_file(&path);
}
}
ui.separator();
// Streams GFS frames out of `s3://noaa-gfs-bdp-pds`
// into either a `.wcav` (encoded on the fly) or
// a `.grib2` (raw concatenation) in a worker
// thread, with a cancel button wired to an
// `AtomicBool` that the fetch loop checks every
// frame.
if ui.button("Fetch NOAA GFS...").clicked() {
ui.close();
self.editor.fetch_dialog.open = true;
self.fetch_job.reset_log();
}
ui.separator();
if ui.button("Load Scenario (TOML)...").clicked() {
ui.close();
if let Some(path) = rfd::FileDialog::new()
.add_filter("TOML", &["toml"])
.pick_file()
{
self.load_scenario_from_file(&path);
}
}
if ui.button("Save Scenario (TOML)...").clicked() {
ui.close();
if let Some(path) = rfd::FileDialog::new()
.add_filter("TOML", &["toml"])
.set_file_name("scenario.toml")
.save_file()
{
self.save_scenario_to_file(&path);
}
}
ui.separator();
if ui.button("Load Solution...").clicked() {
ui.close();
if let Some(path) = rfd::FileDialog::new()
.add_filter("JSON", &["json"])
.pick_file()
{
self.load_solution_from_file(&path);
}
}
let has_solution = self.outputs.route_evolution.is_some();
if ui
.add_enabled(has_solution, egui::Button::new("Save Solution..."))
.on_disabled_hover_text(
"Run a search first to produce a route to save.",
)
.clicked()
{
ui.close();
if let Some(path) = rfd::FileDialog::new()
.add_filter("JSON", &["json"])
.set_file_name("solution.json")
.save_file()
{
self.save_solution_to_file(&path);
}
}
ui.separator();
// Reload the `wind_av1` sample dataset.
// For builds with the file embedded
// (`build.rs` saw `assets/sample_wind.wcav`
// at compile time) this is an in-binary
// decode; for builds without it, the
// sample is pulled from raw.githubusercontent
// on first hit and cached locally for
// subsequent reloads — see `bundled_sample`.
let can_load = crate::bundled_sample::can_load_sample();
let has_bundled = crate::bundled_sample::has_bundled_sample();
let label = if self.bundled_sample_job.is_running() {
"Reload Sample (working…)"
} else if has_bundled {
"Reload Sample"
} else {
"Reload Sample (downloads if not cached)"
};
let disabled_hint = if !can_load {
"This build target doesn't support sample loading."
} else {
"A sample load is already running."
};
if ui
.add_enabled(
can_load && !self.bundled_sample_job.is_running(),
egui::Button::new(label),
)
.on_disabled_hover_text(disabled_hint)
.clicked()
{
ui.close();
let ctx = ui.ctx().clone();
self.start_bundled_sample_decode(&ctx);
}
ui.separator();
}
if ui.button("Quit").clicked() {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.add_space(16.0);
}
ui.menu_button("Advanced", |ui| {
// Force the dropdown wide enough for the longest entry;
// egui sizes menus to fit content but rounds down on
// some font/zoom combos and the labels end up clipped.
ui.set_min_width(180.0);
if ui.button("Advanced Params").clicked() {
ui.close();
self.editor.advanced_settings_open = true;
}
if ui.button("Generate Wind Map").clicked() {
ui.close();
self.editor.generate_window_open = true;
}
});
ui.add_space(16.0);
egui::widgets::global_theme_preference_buttons(ui);
});
});
}
}