aboutsummaryrefslogtreecommitdiffstats
path: root/ui-desktop/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'ui-desktop/src/main/java')
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/App.java16
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/AppState.java760
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/AppView.java260
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/ServiceEngine.java71
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/Skin.java23
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/AppTab.java6
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/CommissionFilter.java7
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListCell.java29
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListItemView.java51
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/LadderItemView.java37
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/MechanismFilter.java7
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/ModalDialog.java8
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/ParticipantItemView.java32
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeHistoryItemView.java38
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeSide.java6
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/UserEventItemView.java38
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListCell.java29
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListItemView.java34
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/AnimationToggleGraphic.java44
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/EqualizerGraphic.java42
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Graphic.java65
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/GraphicHelper.java64
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChart.java28
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChartGraphic.java121
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Motion.java59
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/SparklineGraphic.java65
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/controllers/AppController.java592
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/controllers/CreateEventDialogController.java163
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EmptyStateController.java14
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EventsTabController.java496
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/controllers/LoadDialogController.java70
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/controllers/ResolveDialogController.java68
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/controllers/UsersTabController.java476
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/model/BookRow.java3
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/model/ChartPoint.java3
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/model/EventData.java81
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/model/OrderBook.java16
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/model/ParticipantRow.java4
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/model/TradeRow.java4
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/model/UserData.java41
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/model/UserEventRow.java4
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/task/InitialLoadTask.java57
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/util/Charts.java202
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/util/Format.java21
-rw-r--r--ui-desktop/src/main/java/market/guess/ui/desktop/util/Views.java41
45 files changed, 4283 insertions, 13 deletions
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/App.java b/ui-desktop/src/main/java/market/guess/ui/desktop/App.java
index f0b2b7a..0d92c7d 100644
--- a/ui-desktop/src/main/java/market/guess/ui/desktop/App.java
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/App.java
@@ -1,22 +1,12 @@
1package market.guess.ui.desktop; 1package market.guess.ui.desktop;
2 2
3import javafx.application.Application; 3import javafx.application.Application;
4import javafx.scene.Scene;
5import javafx.scene.control.Label;
6import javafx.scene.layout.StackPane;
7import javafx.stage.Stage; 4import javafx.stage.Stage;
8 5
9public class App extends Application { 6public final class App extends Application {
10 @Override 7 @Override
11 public void start(Stage stage) throws Exception { 8 public void start(Stage stage) {
12 var javaVersion = System.getProperty("java.version"); 9 new AppView(stage);
13 var openjfxVersion = System.getProperty("javafx.version");
14
15 var label =
16 new Label("Hello. JavaFX " + openjfxVersion + ", running on Java " + javaVersion + ".");
17 var scene = new Scene(new StackPane(label), 640, 480);
18
19 stage.setScene(scene);
20 stage.show(); 10 stage.show();
21 } 11 }
22} 12}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/AppState.java b/ui-desktop/src/main/java/market/guess/ui/desktop/AppState.java
new file mode 100644
index 0000000..80e472b
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/AppState.java
@@ -0,0 +1,760 @@
1package market.guess.ui.desktop;
2
3import java.math.BigDecimal;
4import java.time.Instant;
5import java.time.LocalDateTime;
6import java.time.ZoneId;
7import java.time.format.DateTimeFormatter;
8import java.util.ArrayList;
9import java.util.Collections;
10import java.util.List;
11import javafx.beans.property.BooleanProperty;
12import javafx.beans.property.ObjectProperty;
13import javafx.beans.property.SimpleBooleanProperty;
14import javafx.beans.property.SimpleObjectProperty;
15import javafx.beans.property.SimpleStringProperty;
16import javafx.beans.property.StringProperty;
17import javafx.collections.FXCollections;
18import javafx.collections.ListChangeListener;
19import javafx.collections.ObservableList;
20import market.guess.api.AccountContext;
21import market.guess.api.CatalogContext;
22import market.guess.api.GuessMarketContext;
23import market.guess.model.event.EventDetailDTO;
24import market.guess.model.event.EventStatus;
25import market.guess.model.event.EventSummaryDTO;
26import market.guess.model.event.MechanismType;
27import market.guess.model.ledger.LedgerDTO;
28import market.guess.ui.desktop.components.AppTab;
29import market.guess.ui.desktop.components.CommissionFilter;
30import market.guess.ui.desktop.components.MechanismFilter;
31import market.guess.ui.desktop.components.ModalDialog;
32import market.guess.ui.desktop.components.TradeSide;
33import market.guess.ui.desktop.model.BookRow;
34import market.guess.ui.desktop.model.ChartPoint;
35import market.guess.ui.desktop.model.EventData;
36import market.guess.ui.desktop.model.OrderBook;
37import market.guess.ui.desktop.model.ParticipantRow;
38import market.guess.ui.desktop.model.TradeRow;
39import market.guess.ui.desktop.model.UserData;
40import market.guess.ui.desktop.model.UserEventRow;
41import market.guess.ui.desktop.util.Format;
42
43/** Reactive UI state backed by JavaFX properties and observable collections. */
44public final class AppState {
45 private final CatalogContext catalogContext;
46 private final GuessMarketContext marketContext;
47 private final AccountContext accountContext;
48
49 private final StringProperty loadedFile = new SimpleStringProperty(null);
50
51 public final ObservableList<EventData> events = FXCollections.observableArrayList();
52 public final ObservableList<UserData> users = FXCollections.observableArrayList();
53
54 private final StringProperty selectedEventId = new SimpleStringProperty(null);
55 private final ObjectProperty<EventData> selectedEvent = new SimpleObjectProperty<>();
56
57 private final StringProperty actingUserName = new SimpleStringProperty(null);
58 private final ObjectProperty<UserData> actingUser = new SimpleObjectProperty<>();
59
60 private final StringProperty selectedUserEventId = new SimpleStringProperty("1");
61
62 private final ObjectProperty<MechanismFilter> filterMethod =
63 new SimpleObjectProperty<>(MechanismFilter.ALL);
64 private final ObjectProperty<EventStatus> filterStatus = new SimpleObjectProperty<>(null);
65 private final ObjectProperty<CommissionFilter> filterFee =
66 new SimpleObjectProperty<>(CommissionFilter.ALL);
67
68 private final ObjectProperty<TradeSide> tradeSide = new SimpleObjectProperty<>(TradeSide.BUY);
69 private final StringProperty tradeQty = new SimpleStringProperty("50");
70 private final StringProperty tradePrice = new SimpleStringProperty("0.62");
71 private final BooleanProperty tradeOptionYes = new SimpleBooleanProperty(true);
72
73 private final ObjectProperty<Skin> skin = new SimpleObjectProperty<>(Skin.ROSE_PINE_DAWN);
74 private final BooleanProperty animationsOn = new SimpleBooleanProperty(true);
75
76 private final ObjectProperty<AppTab> activeTab = new SimpleObjectProperty<>(AppTab.EVENTS);
77 private final ObjectProperty<ModalDialog> dialog = new SimpleObjectProperty<>(ModalDialog.NONE);
78
79 private final ObjectProperty<MechanismType> createType =
80 new SimpleObjectProperty<>(MechanismType.LMSR);
81 private final BooleanProperty createMintingOn = new SimpleBooleanProperty(true);
82 private final StringProperty resolveSelectedOption = new SimpleStringProperty(null);
83
84 private final StringProperty toast = new SimpleStringProperty(null);
85 private final StringProperty pendingFile = new SimpleStringProperty(null);
86
87 public AppState() {
88 this(new ServiceEngine());
89 }
90
91 public AppState(ServiceEngine engine) {
92 this(engine.getCatalogContext(), engine.getMarketContext(), engine.getAccountContext());
93 }
94
95 public AppState(
96 CatalogContext catalogContext,
97 GuessMarketContext marketContext,
98 AccountContext accountContext) {
99 this.catalogContext = catalogContext;
100 this.marketContext = marketContext;
101 this.accountContext = accountContext;
102
103 selectedEventId.addListener((obs, oldVal, newVal) -> updateSelectedEvent());
104 events.addListener((ListChangeListener<EventData>) c -> updateSelectedEvent());
105
106 actingUserName.addListener((obs, oldVal, newVal) -> updateActingUser());
107 users.addListener((ListChangeListener<UserData>) c -> updateActingUser());
108
109 refreshData();
110 }
111
112 public void refreshData() {
113 var allEventsRes = catalogContext.getAllEvents();
114 var allAccountsRes = accountContext.getAllAccounts();
115
116 var newEventList = new ArrayList<EventData>();
117 if (allEventsRes != null && allEventsRes.isSuccess() && allEventsRes.getData() != null) {
118 for (var summary : allEventsRes.getData()) {
119 var detailRes = catalogContext.getEvent(summary.key());
120 EventDetailDTO detail =
121 (detailRes != null && detailRes.isSuccess()) ? detailRes.getData() : null;
122 newEventList.add(toEventData(summary, detail));
123 }
124 }
125 this.events.setAll(newEventList);
126
127 var newUserList = new ArrayList<UserData>();
128 if (allAccountsRes != null && allAccountsRes.isSuccess() && allAccountsRes.getData() != null) {
129 for (var acc : allAccountsRes.getData()) {
130 newUserList.add(toUserData(acc, newEventList));
131 }
132 }
133 this.users.setAll(newUserList);
134 updateSelectedEvent();
135 updateActingUser();
136 }
137
138 private EventData toEventData(EventSummaryDTO summary, EventDetailDTO detail) {
139 double contractVal = 0.0;
140 try {
141 if (summary.accountBalance() != null) {
142 contractVal =
143 Double.parseDouble(summary.accountBalance().replace("$", "").replace(",", "").trim());
144 }
145 } catch (Exception ignored) {
146 }
147
148 EventData ev =
149 new EventData(
150 summary.key(),
151 summary.displayId(),
152 summary.name(),
153 summary.description(),
154 summary.mechanism(),
155 summary.status(),
156 summary.marketMaker(),
157 summary.commissionPercent(),
158 summary.commissionTiming(),
159 contractVal,
160 summary.mechanism() == MechanismType.LMSR ? 100 : null,
161 summary.mechanism() == MechanismType.ORDER_BOOK ? BigDecimal.ONE : null);
162
163 if (detail != null) {
164 if (detail.state() != null && detail.state().markets() != null) {
165 var markets = detail.state().markets();
166 if (markets.size() >= 1) {
167 try {
168 ev.yesPrice = new BigDecimal(markets.get(0).price());
169 ev.yesShares = (int) Double.parseDouble(markets.get(0).volume());
170 } catch (Exception ignored) {
171 }
172 }
173 if (markets.size() >= 2) {
174 try {
175 ev.noPrice = new BigDecimal(markets.get(1).price());
176 ev.noShares = (int) Double.parseDouble(markets.get(1).volume());
177 } catch (Exception ignored) {
178 }
179 }
180 }
181 if (detail.history() != null) {
182 int idx = 1;
183 for (var t : detail.history()) {
184 boolean isYes =
185 "YES".equalsIgnoreCase(t.optionName())
186 || (summary.optionNames() != null
187 && !summary.optionNames().isEmpty()
188 && summary.optionNames().get(0).equalsIgnoreCase(t.optionName()));
189 int qty = 0;
190 try {
191 qty = (int) Long.parseLong(t.quantity());
192 } catch (Exception ignored) {
193 }
194 ev.trades.add(
195 new TradeRow(idx++, t.userName(), t.optionName(), isYes, qty, "$" + t.pricePaid()));
196 }
197 }
198 ev.resolvedOption = detail.settledMarket();
199 }
200
201 // Baseline and historical chart points
202 if (detail != null && detail.history() != null && !detail.history().isEmpty()) {
203 var historyChronological = new ArrayList<>(detail.history());
204 Collections.reverse(historyChronological);
205
206 long firstT = parseEpochSecond(historyChronological.get(0).at());
207 if (firstT == 0L) firstT = Instant.now().getEpochSecond();
208 ev.chart.add(new ChartPoint(firstT - 60, 0.50));
209
210 for (var t : historyChronological) {
211 long time = parseEpochSecond(t.at());
212 if (time == 0L) time = firstT;
213 boolean isYes =
214 "YES".equalsIgnoreCase(t.optionName())
215 || (summary.optionNames() != null
216 && !summary.optionNames().isEmpty()
217 && summary.optionNames().get(0).equalsIgnoreCase(t.optionName()));
218 double price = 0.50;
219 try {
220 double cost = Double.parseDouble(t.pricePaid().replace("$", "").replace(",", "").trim());
221 long qty = Long.parseLong(t.quantity());
222 if (qty > 0) {
223 double unitP = cost / qty;
224 price = isYes ? unitP : Math.max(0.0, 1.0 - unitP);
225 }
226 } catch (Exception ignored) {
227 price = (ev.yesPrice != null) ? ev.yesPrice.doubleValue() : 0.50;
228 }
229 ev.chart.add(new ChartPoint(time, Math.max(0.0, Math.min(1.0, price))));
230 }
231 } else {
232 long now = Instant.now().getEpochSecond();
233 double p = (ev.yesPrice != null) ? ev.yesPrice.doubleValue() : 0.50;
234 ev.chart.add(new ChartPoint(now - 60, p));
235 ev.chart.add(new ChartPoint(now, p));
236 }
237
238 // Order books for OB
239 if (ev.type == MechanismType.ORDER_BOOK) {
240 String yesLast = ev.yesPrice != null ? Format.money(ev.yesPrice) : "$0.50";
241 String noLast = ev.noPrice != null ? Format.money(ev.noPrice) : "$0.50";
242 ev.yesBook = new OrderBook("YES", true);
243 ev.yesBook.last = yesLast;
244 ev.yesBook.bid = "—";
245 ev.yesBook.ask = "—";
246 ev.yesBook.mid = yesLast;
247 ev.yesBook.spread = "—";
248
249 ev.noBook = new OrderBook("NO", false);
250 ev.noBook.last = noLast;
251 ev.noBook.bid = "—";
252 ev.noBook.ask = "—";
253 ev.noBook.mid = noLast;
254 ev.noBook.spread = "—";
255
256 if (detail != null && detail.orderBooks() != null && !detail.orderBooks().isEmpty()) {
257 for (var obDto : detail.orderBooks()) {
258 boolean isYes =
259 "YES".equalsIgnoreCase(obDto.optionKey())
260 || "YES".equalsIgnoreCase(obDto.optionName())
261 || obDto.optionKey().endsWith(":0")
262 || obDto.optionKey().equals("0");
263 OrderBook target = isYes ? ev.yesBook : ev.noBook;
264 if (target != null) {
265 if (obDto.bestBid() != null) target.bid = obDto.bestBid();
266 if (obDto.bestAsk() != null) target.ask = obDto.bestAsk();
267 if (obDto.spread() != null) target.spread = obDto.spread();
268 target.mid = midOf(target.bid, target.ask);
269 target.rows.clear();
270 if (obDto.orders() != null) {
271 for (var rowDto : obDto.orders()) {
272 target.rows.add(
273 new BookRow(
274 rowDto.side(),
275 rowDto.user(),
276 (int) rowDto.quantity(),
277 rowDto.price(),
278 rowDto.isBid()));
279 }
280 }
281 }
282 }
283 }
284 }
285
286 // Participants
287 if (detail != null && detail.participants() != null && !detail.participants().isEmpty()) {
288 ev.participants.clear();
289 for (var p : detail.participants()) {
290 ev.participants.add(
291 new ParticipantRow(
292 p.user(), p.tag(), p.yes(), p.no(), p.value(), p.fees(), p.pnl()));
293 }
294 } else if (ev.participants.isEmpty() && ev.mm != null && !ev.mm.isBlank()) {
295 ev.participants.add(new ParticipantRow(ev.mm, "MM", 0, 0, "$0.00", "$0.00", "$0.00"));
296 }
297
298 return ev;
299 }
300
301 private UserData toUserData(LedgerDTO acc, List<EventData> allEvents) {
302 double bal = 0.0;
303 try {
304 if (acc.balance() != null) {
305 bal = Double.parseDouble(acc.balance().replace("$", "").replace(",", "").trim());
306 }
307 } catch (Exception ignored) {
308 }
309
310 boolean isMm =
311 allEvents.stream().anyMatch(e -> e.mm != null && acc.owner().equalsIgnoreCase(e.mm));
312 String role = isMm ? "Market Maker" : "Trader";
313
314 UserData u = new UserData(acc.owner(), role, bal, acc.blocked(), isMm);
315
316 if (acc.entries() != null && !acc.entries().isEmpty()) {
317 var entries = acc.entries();
318 long firstT = parseEpochSecond(entries.get(0).at());
319 if (firstT == 0L) firstT = Instant.now().getEpochSecond();
320
321 double firstAmount = 0.0;
322 double firstBalAfter = 0.0;
323 try {
324 firstAmount =
325 Double.parseDouble(entries.get(0).amount().replace("$", "").replace(",", "").trim());
326 firstBalAfter =
327 Double.parseDouble(
328 entries.get(0).balanceAfter().replace("$", "").replace(",", "").trim());
329 } catch (Exception ignored) {
330 }
331 double initialBal = firstBalAfter - firstAmount;
332 u.balanceHistory.add(new ChartPoint(firstT - 60, Math.max(0.0, initialBal)));
333
334 for (var entry : entries) {
335 long t = parseEpochSecond(entry.at());
336 if (t == 0L) t = firstT;
337 try {
338 double bAfter =
339 Double.parseDouble(entry.balanceAfter().replace("$", "").replace(",", "").trim());
340 u.balanceHistory.add(new ChartPoint(t, bAfter));
341 } catch (Exception ignored) {
342 }
343 }
344 } else {
345 long now = Instant.now().getEpochSecond();
346 u.balanceHistory.add(new ChartPoint(now - 60, bal));
347 u.balanceHistory.add(new ChartPoint(now, bal));
348 }
349
350 for (var ev : allEvents) {
351 String typeStr = ev.type == MechanismType.LMSR ? "LMSR" : "Order Book";
352 boolean isEvMm = ev.mm != null && acc.owner().equalsIgnoreCase(ev.mm);
353 long yesShares = 0;
354 long noShares = 0;
355 var pOpt =
356 ev.participants.stream().filter(p -> p.user().equalsIgnoreCase(acc.owner())).findFirst();
357 if (pOpt.isPresent()) {
358 yesShares = pOpt.get().yes();
359 noShares = pOpt.get().no();
360 } else {
361 if (isEvMm && ev.type == MechanismType.ORDER_BOOK) {
362 yesShares += 100;
363 noShares += 100;
364 }
365 for (var trade : ev.trades) {
366 if (acc.owner().equalsIgnoreCase(trade.user())) {
367 if (trade.yesOption()) yesShares += trade.shares();
368 else noShares += trade.shares();
369 }
370 }
371 }
372 // Spec: P/L is reported once the event is closed; until then there is no realised result.
373 String valStr =
374 ev.status != EventStatus.SETTLED ? "—" : pOpt.isPresent() ? pOpt.get().pnl() : "$0.00";
375 if (isEvMm) {
376 u.events.add(
377 new UserEventRow(
378 ev.id,
379 ev.name,
380 "Market Maker",
381 typeStr,
382 (int) Math.max(0, yesShares),
383 (int) Math.max(0, noShares),
384 valStr));
385 } else if (yesShares > 0 || noShares > 0) {
386 u.events.add(
387 new UserEventRow(
388 ev.id,
389 ev.name,
390 "Trader",
391 typeStr,
392 (int) Math.max(0, yesShares),
393 (int) Math.max(0, noShares),
394 valStr));
395 }
396 }
397
398 return u;
399 }
400
401 /** Mid price between best bid and best ask, or "—" when either side of the book is empty. */
402 static String midOf(String bid, String ask) {
403 try {
404 var b = new BigDecimal(bid.replace("$", "").trim());
405 var a = new BigDecimal(ask.replace("$", "").trim());
406 return Format.money(b.add(a).divide(BigDecimal.TWO));
407 } catch (RuntimeException e) {
408 return "—";
409 }
410 }
411
412 public static long parseEpochSecond(String timeStr) {
413 if (timeStr == null || timeStr.isBlank()) return 0L;
414 try {
415 return LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
416 .atZone(ZoneId.systemDefault())
417 .toEpochSecond();
418 } catch (Exception e) {
419 try {
420 return Instant.parse(timeStr).getEpochSecond();
421 } catch (Exception ignored) {
422 return 0L;
423 }
424 }
425 }
426
427 private void updateSelectedEvent() {
428 String id = selectedEventId.get();
429 EventData ev = (id != null) ? event(id) : null;
430 if (ev == null && !events.isEmpty()) {
431 ev = events.get(0);
432 if (ev != null) {
433 selectedEventId.set(ev.id);
434 }
435 }
436 selectedEvent.set(null);
437 selectedEvent.set(ev);
438 }
439
440 private void updateActingUser() {
441 String name = actingUserName.get();
442 UserData u = (name != null) ? user(name) : null;
443 if (u == null && !users.isEmpty()) {
444 u = users.get(0);
445 if (u != null) {
446 actingUserName.set(u.name);
447 }
448 }
449 actingUser.set(null);
450 actingUser.set(u);
451 }
452
453 public CatalogContext getCatalogContext() {
454 return catalogContext;
455 }
456
457 public GuessMarketContext getMarketContext() {
458 return marketContext;
459 }
460
461 public AccountContext getAccountContext() {
462 return accountContext;
463 }
464
465 public ObservableList<EventData> getEvents() {
466 return events;
467 }
468
469 public ObservableList<UserData> getUsers() {
470 return users;
471 }
472
473 public StringProperty loadedFileProperty() {
474 return loadedFile;
475 }
476
477 public String getLoadedFile() {
478 return loadedFile.get();
479 }
480
481 public void setLoadedFile(String file) {
482 this.loadedFile.set(file);
483 }
484
485 public StringProperty selectedEventIdProperty() {
486 return selectedEventId;
487 }
488
489 public String getSelectedEventId() {
490 return selectedEventId.get();
491 }
492
493 public void setSelectedEventId(String id) {
494 this.selectedEventId.set(id);
495 }
496
497 public ObjectProperty<EventData> selectedEventProperty() {
498 return selectedEvent;
499 }
500
501 public EventData selectedEvent() {
502 return selectedEvent.get();
503 }
504
505 public EventData getSelectedEvent() {
506 return selectedEvent.get();
507 }
508
509 public void setSelectedEvent(EventData e) {
510 this.selectedEvent.set(e);
511 if (e != null) {
512 this.selectedEventId.set(e.id);
513 }
514 }
515
516 public StringProperty actingUserNameProperty() {
517 return actingUserName;
518 }
519
520 public String getActingUserName() {
521 return actingUserName.get();
522 }
523
524 public void setActingUserName(String name) {
525 this.actingUserName.set(name);
526 }
527
528 public ObjectProperty<UserData> actingUserProperty() {
529 return actingUser;
530 }
531
532 public UserData actingUser() {
533 return actingUser.get();
534 }
535
536 public UserData getActingUser() {
537 return actingUser.get();
538 }
539
540 public void setActingUser(UserData u) {
541 this.actingUser.set(u);
542 if (u != null) {
543 this.actingUserName.set(u.name);
544 }
545 }
546
547 public StringProperty selectedUserEventIdProperty() {
548 return selectedUserEventId;
549 }
550
551 public String getSelectedUserEventId() {
552 return selectedUserEventId.get();
553 }
554
555 public void setSelectedUserEventId(String id) {
556 this.selectedUserEventId.set(id);
557 }
558
559 public ObjectProperty<MechanismFilter> filterMethodProperty() {
560 return filterMethod;
561 }
562
563 public MechanismFilter getFilterMethod() {
564 return filterMethod.get();
565 }
566
567 public void setFilterMethod(MechanismFilter m) {
568 this.filterMethod.set(m);
569 }
570
571 public ObjectProperty<EventStatus> filterStatusProperty() {
572 return filterStatus;
573 }
574
575 public EventStatus getFilterStatus() {
576 return filterStatus.get();
577 }
578
579 public void setFilterStatus(EventStatus s) {
580 this.filterStatus.set(s);
581 }
582
583 public ObjectProperty<CommissionFilter> filterFeeProperty() {
584 return filterFee;
585 }
586
587 public CommissionFilter getFilterFee() {
588 return filterFee.get();
589 }
590
591 public void setFilterFee(CommissionFilter f) {
592 this.filterFee.set(f);
593 }
594
595 public ObjectProperty<TradeSide> tradeSideProperty() {
596 return tradeSide;
597 }
598
599 public TradeSide getTradeSide() {
600 return tradeSide.get();
601 }
602
603 public void setTradeSide(TradeSide s) {
604 this.tradeSide.set(s);
605 }
606
607 public StringProperty tradeQtyProperty() {
608 return tradeQty;
609 }
610
611 public String getTradeQty() {
612 return tradeQty.get();
613 }
614
615 public void setTradeQty(String q) {
616 this.tradeQty.set(q);
617 }
618
619 public StringProperty tradePriceProperty() {
620 return tradePrice;
621 }
622
623 public String getTradePrice() {
624 return tradePrice.get();
625 }
626
627 public void setTradePrice(String p) {
628 this.tradePrice.set(p);
629 }
630
631 public BooleanProperty tradeOptionYesProperty() {
632 return tradeOptionYes;
633 }
634
635 public boolean isTradeOptionYes() {
636 return tradeOptionYes.get();
637 }
638
639 public void setTradeOptionYes(boolean y) {
640 this.tradeOptionYes.set(y);
641 }
642
643 public ObjectProperty<Skin> skinProperty() {
644 return skin;
645 }
646
647 public Skin getSkin() {
648 return skin.get();
649 }
650
651 public void setSkin(Skin s) {
652 this.skin.set(s);
653 }
654
655 public BooleanProperty animationsOnProperty() {
656 return animationsOn;
657 }
658
659 public boolean isAnimationsOn() {
660 return animationsOn.get();
661 }
662
663 public void setAnimationsOn(boolean a) {
664 this.animationsOn.set(a);
665 }
666
667 public ObjectProperty<AppTab> activeTabProperty() {
668 return activeTab;
669 }
670
671 public AppTab getActiveTab() {
672 return activeTab.get();
673 }
674
675 public void setActiveTab(AppTab t) {
676 this.activeTab.set(t);
677 }
678
679 public ObjectProperty<ModalDialog> dialogProperty() {
680 return dialog;
681 }
682
683 public ModalDialog getDialog() {
684 return dialog.get();
685 }
686
687 public void setDialog(ModalDialog d) {
688 this.dialog.set(d);
689 }
690
691 public ObjectProperty<MechanismType> createTypeProperty() {
692 return createType;
693 }
694
695 public MechanismType getCreateType() {
696 return createType.get();
697 }
698
699 public void setCreateType(MechanismType t) {
700 this.createType.set(t);
701 }
702
703 public BooleanProperty createMintingOnProperty() {
704 return createMintingOn;
705 }
706
707 public boolean isCreateMintingOn() {
708 return createMintingOn.get();
709 }
710
711 public void setCreateMintingOn(boolean m) {
712 this.createMintingOn.set(m);
713 }
714
715 public StringProperty resolveSelectedOptionProperty() {
716 return resolveSelectedOption;
717 }
718
719 public String getResolveSelectedOption() {
720 return resolveSelectedOption.get();
721 }
722
723 public void setResolveSelectedOption(String o) {
724 this.resolveSelectedOption.set(o);
725 }
726
727 public StringProperty toastProperty() {
728 return toast;
729 }
730
731 public String getToast() {
732 return toast.get();
733 }
734
735 public void setToast(String t) {
736 this.toast.set(t);
737 }
738
739 public StringProperty pendingFileProperty() {
740 return pendingFile;
741 }
742
743 public String getPendingFile() {
744 return pendingFile.get();
745 }
746
747 public void setPendingFile(String f) {
748 this.pendingFile.set(f);
749 }
750
751 public EventData event(String id) {
752 if (id == null) return null;
753 return events.stream().filter(e -> e.getId().equals(id)).findFirst().orElse(null);
754 }
755
756 public UserData user(String name) {
757 if (name == null) return null;
758 return users.stream().filter(u -> u.getName().equals(name)).findFirst().orElse(null);
759 }
760}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/AppView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/AppView.java
new file mode 100644
index 0000000..f5cf26f
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/AppView.java
@@ -0,0 +1,260 @@
1package market.guess.ui.desktop;
2
3import javafx.fxml.FXMLLoader;
4import javafx.geometry.Pos;
5import javafx.scene.Cursor;
6import javafx.scene.Scene;
7import javafx.scene.control.ScrollPane;
8import javafx.scene.layout.Region;
9import javafx.scene.layout.StackPane;
10import javafx.scene.paint.Color;
11import javafx.scene.shape.Rectangle;
12import javafx.stage.Stage;
13import javafx.stage.StageStyle;
14import market.guess.api.AccountContext;
15import market.guess.api.CatalogContext;
16import market.guess.api.GuessMarketContext;
17import market.guess.ui.desktop.controllers.AppController;
18
19public final class AppView {
20 private static final double CONTENT_MIN_WIDTH = 1040;
21 private static final double CONTENT_MIN_HEIGHT = 560;
22 private static final double WINDOW_MIN_WIDTH = 400;
23 private static final double WINDOW_MIN_HEIGHT = 300;
24
25 private final Stage stage;
26 private final CatalogContext catalogContext;
27 private final GuessMarketContext marketContext;
28 private final AccountContext accountContext;
29 private final AppState state;
30 private AppController controller;
31
32 public AppView(Stage stage) {
33 this(stage, new ServiceEngine());
34 }
35
36 public AppView(Stage stage, ServiceEngine engine) {
37 this(stage, engine.getCatalogContext(), engine.getMarketContext(), engine.getAccountContext());
38 }
39
40 public AppView(
41 Stage stage,
42 CatalogContext catalogContext,
43 GuessMarketContext marketContext,
44 AccountContext accountContext) {
45 this.stage = stage;
46 this.catalogContext = catalogContext;
47 this.marketContext = marketContext;
48 this.accountContext = accountContext;
49 this.state = new AppState(catalogContext, marketContext, accountContext);
50 stage.initStyle(StageStyle.TRANSPARENT);
51 initScene();
52 }
53
54 public CatalogContext getCatalogContext() {
55 return catalogContext;
56 }
57
58 public GuessMarketContext getMarketContext() {
59 return marketContext;
60 }
61
62 public AccountContext getAccountContext() {
63 return accountContext;
64 }
65
66 private void initScene() {
67 try {
68 var loader = new FXMLLoader(getClass().getResource("app_view.fxml"));
69 StackPane root = loader.load();
70 this.controller = loader.getController();
71 this.controller.init(this);
72
73 var clip = new Rectangle();
74 clip.setArcWidth(22);
75 clip.setArcHeight(22);
76 clip.widthProperty().bind(root.widthProperty());
77 clip.heightProperty().bind(root.heightProperty());
78 root.setClip(clip);
79
80 addResizeHandles(root);
81
82 var scene = new Scene(root, 1280, 800);
83 scene.setFill(Color.TRANSPARENT);
84 scene.getStylesheets().add(getClass().getResource("theme.css").toExternalForm());
85 stage.setScene(scene);
86 stage.setTitle("Guess Market");
87 stage.setMinWidth(WINDOW_MIN_WIDTH);
88 stage.setMinHeight(WINDOW_MIN_HEIGHT);
89 // lookup() can't see ScrollPane content until its skin exists, so go through getContent().
90 var appScroll = (ScrollPane) root.lookup(".gm-app-scroll");
91 ((Region) appScroll.getContent()).setMinSize(CONTENT_MIN_WIDTH, CONTENT_MIN_HEIGHT);
92
93 controller.refresh();
94 } catch (Exception e) {
95 throw new RuntimeException("Failed to load app_view.fxml", e);
96 }
97 }
98
99 public Stage getStage() {
100 return stage;
101 }
102
103 public AppState getState() {
104 return state;
105 }
106
107 public void show() {
108 stage.show();
109 }
110
111 private static final double RESIZE_MARGIN = 6;
112 private static final double RESIZE_CORNER = 12;
113
114 private void addResizeHandles(StackPane stack) {
115 stack
116 .getChildren()
117 .addAll(
118 resizeHandle(
119 stack,
120 Cursor.N_RESIZE,
121 Pos.TOP_CENTER,
122 -1,
123 RESIZE_MARGIN,
124 false,
125 false,
126 true,
127 false),
128 resizeHandle(
129 stack,
130 Cursor.S_RESIZE,
131 Pos.BOTTOM_CENTER,
132 -1,
133 RESIZE_MARGIN,
134 false,
135 false,
136 false,
137 true),
138 resizeHandle(
139 stack,
140 Cursor.W_RESIZE,
141 Pos.CENTER_LEFT,
142 RESIZE_MARGIN,
143 -1,
144 true,
145 false,
146 false,
147 false),
148 resizeHandle(
149 stack,
150 Cursor.E_RESIZE,
151 Pos.CENTER_RIGHT,
152 RESIZE_MARGIN,
153 -1,
154 false,
155 true,
156 false,
157 false),
158 resizeHandle(
159 stack,
160 Cursor.NW_RESIZE,
161 Pos.TOP_LEFT,
162 RESIZE_CORNER,
163 RESIZE_CORNER,
164 true,
165 false,
166 true,
167 false),
168 resizeHandle(
169 stack,
170 Cursor.NE_RESIZE,
171 Pos.TOP_RIGHT,
172 RESIZE_CORNER,
173 RESIZE_CORNER,
174 false,
175 true,
176 true,
177 false),
178 resizeHandle(
179 stack,
180 Cursor.SW_RESIZE,
181 Pos.BOTTOM_LEFT,
182 RESIZE_CORNER,
183 RESIZE_CORNER,
184 true,
185 false,
186 false,
187 true),
188 resizeHandle(
189 stack,
190 Cursor.SE_RESIZE,
191 Pos.BOTTOM_RIGHT,
192 RESIZE_CORNER,
193 RESIZE_CORNER,
194 false,
195 true,
196 false,
197 true));
198 }
199
200 private Region resizeHandle(
201 StackPane stack,
202 Cursor cursor,
203 Pos pos,
204 double w,
205 double h,
206 boolean left,
207 boolean right,
208 boolean top,
209 boolean bottom) {
210 var r = new Region();
211 r.setCursor(cursor);
212 r.setMouseTransparent(false);
213 if (w < 0) {
214 r.prefWidthProperty().bind(stack.widthProperty());
215 } else {
216 r.setPrefWidth(w);
217 r.setMaxWidth(w);
218 }
219 if (h < 0) {
220 r.prefHeightProperty().bind(stack.heightProperty());
221 } else {
222 r.setPrefHeight(h);
223 r.setMaxHeight(h);
224 }
225 StackPane.setAlignment(r, pos);
226
227 double[] start = new double[6];
228 r.setOnMousePressed(
229 e -> {
230 if (stage.isMaximized()) return;
231 start[0] = e.getScreenX();
232 start[1] = e.getScreenY();
233 start[2] = stage.getX();
234 start[3] = stage.getY();
235 start[4] = stage.getWidth();
236 start[5] = stage.getHeight();
237 e.consume();
238 });
239 r.setOnMouseDragged(
240 e -> {
241 if (stage.isMaximized()) return;
242 double dx = e.getScreenX() - start[0];
243 double dy = e.getScreenY() - start[1];
244 if (right) stage.setWidth(Math.max(stage.getMinWidth(), start[4] + dx));
245 if (bottom) stage.setHeight(Math.max(stage.getMinHeight(), start[5] + dy));
246 if (left) {
247 double newW = Math.max(stage.getMinWidth(), start[4] - dx);
248 stage.setX(start[2] + (start[4] - newW));
249 stage.setWidth(newW);
250 }
251 if (top) {
252 double newH = Math.max(stage.getMinHeight(), start[5] - dy);
253 stage.setY(start[3] + (start[5] - newH));
254 stage.setHeight(newH);
255 }
256 e.consume();
257 });
258 return r;
259 }
260}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/ServiceEngine.java b/ui-desktop/src/main/java/market/guess/ui/desktop/ServiceEngine.java
new file mode 100644
index 0000000..549991e
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/ServiceEngine.java
@@ -0,0 +1,71 @@
1package market.guess.ui.desktop;
2
3import java.time.Clock;
4import market.guess.api.AccountContext;
5import market.guess.api.CatalogContext;
6import market.guess.api.GuessMarketContext;
7import market.guess.service.LocalGuessMarketContext;
8import market.guess.service.catalog.LocalCatalogContext;
9import market.guess.service.catalog.infrastructure.MarketContext;
10import market.guess.service.catalog.infrastructure.mapper.v2.EventMapperV2;
11import market.guess.service.catalog.infrastructure.provider.Loader;
12import market.guess.service.catalog.infrastructure.provider.v2.XMLLoaderV2;
13import market.guess.service.catalog.infrastructure.provider.v2.XMLValidatorV2;
14import market.guess.service.catalog.infrastructure.repository.EventRepository;
15import market.guess.service.catalog.infrastructure.repository.InMemoryEventRepository;
16import market.guess.service.catalog.infrastructure.repository.InMemoryUserRepository;
17import market.guess.service.catalog.infrastructure.repository.UserRepository;
18import market.guess.service.fulfillment.FulfillmentContext;
19import market.guess.service.fulfillment.LocalFulfillmentContext;
20import market.guess.service.ledger.LedgerContext;
21import market.guess.service.ledger.LocalAccountContext;
22import market.guess.service.matching.LocalMatchingEngine;
23import market.guess.service.matching.MatchingEngine;
24import market.guess.service.risk.LocalRiskEngine;
25import market.guess.service.risk.RiskEngine;
26import market.guess.service.settlement.LocalSettlementContext;
27import market.guess.service.settlement.SettlementContext;
28
29public final class ServiceEngine {
30 private final CatalogContext catalogContext;
31 private final GuessMarketContext marketContext;
32 private final AccountContext accountContext;
33
34 public ServiceEngine() {
35 this(Clock.systemUTC());
36 }
37
38 public ServiceEngine(Clock clock) {
39 EventRepository eventRepo = new InMemoryEventRepository();
40 UserRepository userRepo = new InMemoryUserRepository();
41 MarketContext marketContextInfrastructure = new MarketContext(eventRepo, userRepo);
42 EventMapperV2 mapper = new EventMapperV2();
43 XMLValidatorV2 validator = new XMLValidatorV2();
44 Loader loader = new XMLLoaderV2(eventRepo, userRepo, mapper, validator);
45
46 this.catalogContext = new LocalCatalogContext(loader, marketContextInfrastructure, eventRepo);
47 this.accountContext = new LocalAccountContext(userRepo);
48
49 LedgerContext ledgerContext = new LedgerContext(userRepo, clock);
50 RiskEngine risk = new LocalRiskEngine(eventRepo);
51 MatchingEngine matching = new LocalMatchingEngine(clock);
52 FulfillmentContext fulfillment = new LocalFulfillmentContext(ledgerContext);
53 SettlementContext settlement = new LocalSettlementContext(ledgerContext);
54
55 this.marketContext =
56 new LocalGuessMarketContext(
57 marketContextInfrastructure, risk, matching, fulfillment, settlement);
58 }
59
60 public CatalogContext getCatalogContext() {
61 return catalogContext;
62 }
63
64 public GuessMarketContext getMarketContext() {
65 return marketContext;
66 }
67
68 public AccountContext getAccountContext() {
69 return accountContext;
70 }
71}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/Skin.java b/ui-desktop/src/main/java/market/guess/ui/desktop/Skin.java
new file mode 100644
index 0000000..b8e12d7
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/Skin.java
@@ -0,0 +1,23 @@
1package market.guess.ui.desktop;
2
3public enum Skin {
4 ROSE_PINE_DAWN("Rosé Pine Dawn", "CaskaydiaCove NF"),
5 CATPPUCCIN("Catppuccin", "JetBrainsMono NF"),
6 GRUVBOX("Gruvbox", "FiraCode Nerd Font");
7
8 private final String displayName;
9 private final String fontFamily;
10
11 Skin(String displayName, String fontFamily) {
12 this.displayName = displayName;
13 this.fontFamily = fontFamily;
14 }
15
16 public String getDisplayName() {
17 return displayName;
18 }
19
20 public String getFontFamily() {
21 return fontFamily;
22 }
23}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/AppTab.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/AppTab.java
new file mode 100644
index 0000000..c17a0a6
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/AppTab.java
@@ -0,0 +1,6 @@
1package market.guess.ui.desktop.components;
2
3public enum AppTab {
4 EVENTS,
5 USERS
6}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/CommissionFilter.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/CommissionFilter.java
new file mode 100644
index 0000000..333a544
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/CommissionFilter.java
@@ -0,0 +1,7 @@
1package market.guess.ui.desktop.components;
2
3public enum CommissionFilter {
4 ALL,
5 PURCHASE,
6 CLOSE
7}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListCell.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListCell.java
new file mode 100644
index 0000000..4f5bb53
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListCell.java
@@ -0,0 +1,29 @@
1package market.guess.ui.desktop.components;
2
3import javafx.scene.control.ContentDisplay;
4import javafx.scene.control.ListCell;
5import market.guess.ui.desktop.model.EventData;
6
7public class EventListCell extends ListCell<EventData> {
8 private final EventListItemView view = new EventListItemView();
9
10 public EventListCell() {
11 setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
12 selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
13 if (getItem() != null) {
14 view.update(getItem(), isNowSelected);
15 }
16 });
17 }
18
19 @Override
20 protected void updateItem(EventData item, boolean empty) {
21 super.updateItem(item, empty);
22 if (empty || item == null) {
23 setGraphic(null);
24 } else {
25 view.update(item, isSelected());
26 setGraphic(view);
27 }
28 }
29}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListItemView.java
new file mode 100644
index 0000000..892dbfc
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListItemView.java
@@ -0,0 +1,51 @@
1package market.guess.ui.desktop.components;
2
3import javafx.css.PseudoClass;
4import javafx.fxml.FXML;
5import javafx.scene.control.Label;
6import javafx.scene.layout.VBox;
7import market.guess.model.event.EventStatus;
8import market.guess.model.event.MechanismType;
9import market.guess.ui.desktop.model.EventData;
10import market.guess.ui.desktop.util.Format;
11import market.guess.ui.desktop.util.Views;
12
13public class EventListItemView extends VBox {
14 private static final PseudoClass ACTIVE = PseudoClass.getPseudoClass("active");
15 private static final PseudoClass CLOSED = PseudoClass.getPseudoClass("closed");
16 private static final PseudoClass IDLE = PseudoClass.getPseudoClass("idle");
17
18 @FXML private Label numLabel;
19 @FXML private Label titleLabel;
20 @FXML private Label typeLabel;
21 @FXML private Label statusLabel;
22 @FXML private Label feeLabel;
23 @FXML private Label contractLabel;
24
25 public EventListItemView() {
26 Views.loadRoot(this, "event_list_item.fxml");
27 }
28
29 public EventListItemView(EventData e, boolean selected) {
30 this();
31 update(e, selected);
32 }
33
34 public void update(EventData e, boolean selected) {
35 numLabel.setText("#" + e.num);
36 titleLabel.setText(e.name);
37 typeLabel.setText(e.type == MechanismType.LMSR ? "LMSR" : "Order Book");
38
39 statusLabel.setText(
40 e.status == EventStatus.ACTIVE
41 ? "ACTIVE"
42 : e.status == EventStatus.SETTLED ? "CLOSED" : "IDLE");
43 statusLabel.pseudoClassStateChanged(ACTIVE, e.status == EventStatus.ACTIVE);
44 statusLabel.pseudoClassStateChanged(CLOSED, e.status == EventStatus.SETTLED);
45 statusLabel.pseudoClassStateChanged(IDLE, e.status == EventStatus.DRAFT);
46
47 feeLabel.setText(e.feeText());
48 contractLabel.setText(Format.money(e.contract));
49 Views.setSelected(this, selected);
50 }
51}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/LadderItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/LadderItemView.java
new file mode 100644
index 0000000..e2ecfbd
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/LadderItemView.java
@@ -0,0 +1,37 @@
1package market.guess.ui.desktop.components;
2
3import javafx.css.PseudoClass;
4import javafx.fxml.FXML;
5import javafx.scene.control.Label;
6import javafx.scene.layout.HBox;
7import market.guess.ui.desktop.model.BookRow;
8import market.guess.ui.desktop.util.Views;
9
10public class LadderItemView extends HBox {
11 private static final PseudoClass BID = PseudoClass.getPseudoClass("bid");
12 private static final PseudoClass ASK = PseudoClass.getPseudoClass("ask");
13
14 @FXML private Label priceLabel;
15 @FXML private Label sideLabel;
16 @FXML private Label qtyLabel;
17 @FXML private Label userLabel;
18
19 public LadderItemView() {
20 Views.loadRoot(this, "ladder_item.fxml");
21 }
22
23 public LadderItemView(BookRow row) {
24 this();
25 update(row);
26 }
27
28 public void update(BookRow r) {
29 priceLabel.setText(r.price());
30 sideLabel.setText(r.side());
31 qtyLabel.setText(String.valueOf(r.qty()));
32 userLabel.setText(r.user());
33
34 pseudoClassStateChanged(BID, r.isBid());
35 pseudoClassStateChanged(ASK, !r.isBid());
36 }
37}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/MechanismFilter.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/MechanismFilter.java
new file mode 100644
index 0000000..7a85304
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/MechanismFilter.java
@@ -0,0 +1,7 @@
1package market.guess.ui.desktop.components;
2
3public enum MechanismFilter {
4 ALL,
5 LMSR,
6 ORDER_BOOK
7}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/ModalDialog.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/ModalDialog.java
new file mode 100644
index 0000000..21865a5
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/ModalDialog.java
@@ -0,0 +1,8 @@
1package market.guess.ui.desktop.components;
2
3public enum ModalDialog {
4 NONE,
5 LOAD,
6 CREATE_EVENT,
7 RESOLVE
8}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/ParticipantItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/ParticipantItemView.java
new file mode 100644
index 0000000..2161cfe
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/ParticipantItemView.java
@@ -0,0 +1,32 @@
1package market.guess.ui.desktop.components;
2
3import javafx.fxml.FXML;
4import javafx.scene.control.Label;
5import javafx.scene.layout.HBox;
6import market.guess.ui.desktop.model.ParticipantRow;
7import market.guess.ui.desktop.util.Views;
8
9public class ParticipantItemView extends HBox {
10 @FXML private Label userLabel;
11 @FXML private Label yesLabel;
12 @FXML private Label noLabel;
13 @FXML private Label valueLabel;
14 @FXML private Label feesLabel;
15
16 public ParticipantItemView() {
17 Views.loadRoot(this, "participant_item.fxml");
18 }
19
20 public ParticipantItemView(ParticipantRow p) {
21 this();
22 update(p);
23 }
24
25 public void update(ParticipantRow p) {
26 userLabel.setText(p.user() + (p.tag().isEmpty() ? "" : " (" + p.tag() + ")"));
27 yesLabel.setText(String.valueOf(p.yes()));
28 noLabel.setText(String.valueOf(p.no()));
29 valueLabel.setText(p.value());
30 feesLabel.setText(p.fees());
31 }
32}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeHistoryItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeHistoryItemView.java
new file mode 100644
index 0000000..f00090c
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeHistoryItemView.java
@@ -0,0 +1,38 @@
1package market.guess.ui.desktop.components;
2
3import javafx.css.PseudoClass;
4import javafx.fxml.FXML;
5import javafx.scene.control.Label;
6import javafx.scene.layout.HBox;
7import market.guess.ui.desktop.model.TradeRow;
8import market.guess.ui.desktop.util.Views;
9
10public class TradeHistoryItemView extends HBox {
11 private static final PseudoClass YES_STATE = PseudoClass.getPseudoClass("yes");
12 private static final PseudoClass NO_STATE = PseudoClass.getPseudoClass("no");
13
14 @FXML private Label numLabel;
15 @FXML private Label userLabel;
16 @FXML private Label optionLabel;
17 @FXML private Label sharesLabel;
18 @FXML private Label paidLabel;
19
20 public TradeHistoryItemView() {
21 Views.loadRoot(this, "trade_history_item.fxml");
22 }
23
24 public TradeHistoryItemView(TradeRow t) {
25 this();
26 update(t);
27 }
28
29 public void update(TradeRow t) {
30 numLabel.setText("#" + t.n());
31 userLabel.setText(t.user());
32 optionLabel.setText(t.option());
33 optionLabel.pseudoClassStateChanged(YES_STATE, t.yesOption());
34 optionLabel.pseudoClassStateChanged(NO_STATE, !t.yesOption());
35 sharesLabel.setText(t.shares() + " shares");
36 paidLabel.setText(t.paid());
37 }
38}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeSide.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeSide.java
new file mode 100644
index 0000000..bf4a5a0
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeSide.java
@@ -0,0 +1,6 @@
1package market.guess.ui.desktop.components;
2
3public enum TradeSide {
4 BUY,
5 SELL
6}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserEventItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserEventItemView.java
new file mode 100644
index 0000000..0e1dea7
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserEventItemView.java
@@ -0,0 +1,38 @@
1package market.guess.ui.desktop.components;
2
3import javafx.css.PseudoClass;
4import javafx.fxml.FXML;
5import javafx.scene.control.Label;
6import javafx.scene.layout.HBox;
7import market.guess.ui.desktop.model.UserEventRow;
8import market.guess.ui.desktop.util.Views;
9
10public class UserEventItemView extends HBox {
11 private static final PseudoClass NEGATIVE = PseudoClass.getPseudoClass("negative");
12
13 @FXML private Label nameLabel;
14 @FXML private Label roleLabel;
15 @FXML private Label typeLabel;
16 @FXML private Label yesLabel;
17 @FXML private Label noLabel;
18 @FXML private Label plLabel;
19
20 public UserEventItemView() {
21 Views.loadRoot(this, "user_event_item.fxml");
22 }
23
24 public UserEventItemView(UserEventRow ev) {
25 this();
26 update(ev);
27 }
28
29 public void update(UserEventRow ev) {
30 nameLabel.setText(ev.eventName());
31 roleLabel.setText(ev.role());
32 typeLabel.setText(ev.type());
33 yesLabel.setText(String.valueOf(ev.yes()));
34 noLabel.setText(String.valueOf(ev.no()));
35 plLabel.setText(ev.pl());
36 plLabel.pseudoClassStateChanged(NEGATIVE, ev.pl().startsWith("-"));
37 }
38}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListCell.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListCell.java
new file mode 100644
index 0000000..390cc77
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListCell.java
@@ -0,0 +1,29 @@
1package market.guess.ui.desktop.components;
2
3import javafx.scene.control.ContentDisplay;
4import javafx.scene.control.ListCell;
5import market.guess.ui.desktop.model.UserData;
6
7public class UserListCell extends ListCell<UserData> {
8 private final UserListItemView view = new UserListItemView();
9
10 public UserListCell() {
11 setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
12 selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
13 if (getItem() != null) {
14 view.update(getItem(), isNowSelected);
15 }
16 });
17 }
18
19 @Override
20 protected void updateItem(UserData item, boolean empty) {
21 super.updateItem(item, empty);
22 if (empty || item == null) {
23 setGraphic(null);
24 } else {
25 view.update(item, isSelected());
26 setGraphic(view);
27 }
28 }
29}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListItemView.java
new file mode 100644
index 0000000..eb6e7a0
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListItemView.java
@@ -0,0 +1,34 @@
1package market.guess.ui.desktop.components;
2
3import javafx.css.PseudoClass;
4import javafx.fxml.FXML;
5import javafx.scene.control.Label;
6import javafx.scene.layout.HBox;
7import market.guess.ui.desktop.model.UserData;
8import market.guess.ui.desktop.util.Format;
9import market.guess.ui.desktop.util.Views;
10
11public class UserListItemView extends HBox {
12 private static final PseudoClass NEGATIVE = PseudoClass.getPseudoClass("negative");
13
14 @FXML private Label nameLabel;
15 @FXML private Label roleLabel;
16 @FXML private Label balanceLabel;
17
18 public UserListItemView() {
19 Views.loadRoot(this, "user_list_item.fxml");
20 }
21
22 public UserListItemView(UserData u, boolean selected) {
23 this();
24 update(u, selected);
25 }
26
27 public void update(UserData u, boolean selected) {
28 nameLabel.setText(u.name);
29 roleLabel.setText(u.role);
30 balanceLabel.setText(Format.money(u.balance));
31 balanceLabel.pseudoClassStateChanged(NEGATIVE, u.balance.signum() < 0);
32 Views.setSelected(this, selected);
33 }
34}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/AnimationToggleGraphic.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/AnimationToggleGraphic.java
new file mode 100644
index 0000000..77059a9
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/AnimationToggleGraphic.java
@@ -0,0 +1,44 @@
1package market.guess.ui.desktop.components.graphic;
2
3import static market.guess.ui.desktop.components.graphic.GraphicHelper.box;
4import static market.guess.ui.desktop.components.graphic.GraphicHelper.styled;
5
6import java.util.function.DoubleConsumer;
7import javafx.scene.Group;
8import javafx.scene.shape.Circle;
9import javafx.scene.shape.SVGPath;
10import javafx.util.Duration;
11
12/**
13 * Spinny graphic for the animation toggle button.
14 * Renders a kinetic 4-blade turbine / pinwheel that spins continuously when active,
15 * and rests stationary at 0 degrees when inactive.
16 */
17public final class AnimationToggleGraphic {
18 private static final Duration CYCLE = Duration.seconds(2.4);
19
20 private static final String BLADES_PATH =
21 "M 10 10 L 10 2 A 8 8 0 0 1 15.65 4.35 Q 12 8 10 10 Z "
22 + "M 10 10 L 18 10 A 8 8 0 0 1 15.65 15.65 Q 12 12 10 10 Z "
23 + "M 10 10 L 10 18 A 8 8 0 0 1 4.35 15.65 Q 8 12 10 10 Z "
24 + "M 10 10 L 2 10 A 8 8 0 0 1 4.35 4.35 Q 8 8 10 10 Z";
25
26 private AnimationToggleGraphic() {}
27
28 public static Graphic create() {
29 var blades = styled(new SVGPath(), "gm-anim-spinner");
30 blades.setContent(BLADES_PATH);
31
32 var hub = styled(new Circle(10, 10, 2.2), "gm-anim-spinner-hub");
33
34 var art = new Group(blades, hub);
35
36 DoubleConsumer render =
37 p -> {
38 // Seamless continuous 360-degree rotation when active; rests at 0 when stopped
39 art.setRotate(p >= 1.0 ? 0.0 : p * 360.0);
40 };
41
42 return new Graphic(box(art, 20, 20, "gm-anim-toggle-graphic"), render, CYCLE);
43 }
44}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/EqualizerGraphic.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/EqualizerGraphic.java
new file mode 100644
index 0000000..16f8735
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/EqualizerGraphic.java
@@ -0,0 +1,42 @@
1package market.guess.ui.desktop.components.graphic;
2
3import static market.guess.ui.desktop.components.graphic.GraphicHelper.at;
4import static market.guess.ui.desktop.components.graphic.GraphicHelper.box;
5import static market.guess.ui.desktop.components.graphic.GraphicHelper.styled;
6
7import java.util.function.DoubleConsumer;
8import javafx.scene.Group;
9import javafx.scene.shape.Rectangle;
10
11/**
12 * Events tab LIVE pill equalizer graphic.
13 */
14public final class EqualizerGraphic {
15 public static final double BAR_H = 12;
16
17 private EqualizerGraphic() {}
18
19 public static Graphic create() {
20 var bars = new Rectangle[3];
21 for (int i = 0; i < bars.length; i++) {
22 var bar = new Rectangle(i * 6, 0, 4, BAR_H);
23 bar.setArcWidth(2);
24 bar.setArcHeight(2);
25 bars[i] = styled(bar, "gm-anim-bar");
26 }
27 DoubleConsumer render =
28 p -> {
29 // Three offset loops: order flow arriving. The final frame leaves all three legible.
30 bar(bars[0], at(p, 0, .35, .30, 1, .60, .6, 1, .35));
31 bar(bars[1], at(p, 0, .8, .25, .3, .70, 1, 1, .8));
32 bar(bars[2], at(p, 0, .55, .45, 1, .80, .4, 1, .55));
33 };
34 return new Graphic(box(new Group(bars), 16, BAR_H, "gm-anim-equalizer"), render);
35 }
36
37 /** Grows the bar from a bottom origin — JavaFX scaleY would pivot at the centre. */
38 private static void bar(Rectangle r, double scale) {
39 r.setHeight(BAR_H * scale);
40 r.setY(BAR_H - BAR_H * scale);
41 }
42}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Graphic.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Graphic.java
new file mode 100644
index 0000000..a337928
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Graphic.java
@@ -0,0 +1,65 @@
1package market.guess.ui.desktop.components.graphic;
2
3import java.util.function.DoubleConsumer;
4import javafx.animation.Animation;
5import javafx.animation.Interpolator;
6import javafx.animation.KeyFrame;
7import javafx.animation.KeyValue;
8import javafx.animation.Timeline;
9import javafx.beans.property.DoubleProperty;
10import javafx.beans.property.SimpleDoubleProperty;
11import javafx.scene.layout.Pane;
12import javafx.util.Duration;
13
14/**
15 * Runtime wrapper for an animated decorative graphic driven by a periodic timeline.
16 */
17public class Graphic {
18 public static final Duration CYCLE = Duration.seconds(3.6);
19
20 public final Pane node;
21
22 public Pane getNode() {
23 return node;
24 }
25
26 private final DoubleConsumer render;
27 private final DoubleProperty progress = new SimpleDoubleProperty(1);
28 private final Timeline timeline;
29
30 public Graphic(Pane node, DoubleConsumer render) {
31 this(node, render, CYCLE);
32 }
33
34 public Graphic(Pane node, DoubleConsumer render, Duration cycle) {
35 this.node = node;
36 this.render = render;
37 progress.addListener((obs, old, val) -> render.accept(val.doubleValue()));
38 timeline =
39 new Timeline(
40 new KeyFrame(Duration.ZERO, new KeyValue(progress, 0.0, Interpolator.LINEAR)),
41 new KeyFrame(cycle, new KeyValue(progress, 1.0, Interpolator.LINEAR)));
42 timeline.setCycleCount(Animation.INDEFINITE);
43 render.accept(1);
44 }
45
46 public void setPlaying(boolean playing) {
47 if (playing == (timeline.getStatus() == Animation.Status.RUNNING)) {
48 return;
49 }
50 timeline.stop();
51 if (playing) {
52 timeline.playFromStart();
53 } else {
54 render.accept(1);
55 }
56 }
57
58 public Timeline getTimeline() {
59 return timeline;
60 }
61
62 public DoubleProperty progressProperty() {
63 return progress;
64 }
65}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/GraphicHelper.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/GraphicHelper.java
new file mode 100644
index 0000000..1dcf2ba
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/GraphicHelper.java
@@ -0,0 +1,64 @@
1package market.guess.ui.desktop.components.graphic;
2
3import javafx.animation.Interpolator;
4import javafx.scene.Node;
5import javafx.scene.layout.Pane;
6import javafx.scene.shape.Circle;
7import javafx.scene.shape.Shape;
8
9/**
10 * Shared utility functions for styling shapes, containers, and interpolation math.
11 */
12public final class GraphicHelper {
13 private GraphicHelper() {}
14
15 public static <T extends Shape> T styled(T shape, String styleClass) {
16 shape.getStyleClass().add(styleClass);
17 return shape;
18 }
19
20 public static Circle dot(double r, String styleClass) {
21 return styled(new Circle(r), styleClass);
22 }
23
24 public static void place(Circle dot, double[] xy) {
25 dot.setCenterX(xy[0]);
26 dot.setCenterY(xy[1]);
27 }
28
29 public static Pane initBox(Pane pane, Node art, double w, double h, String styleClass) {
30 pane.getChildren().setAll(art);
31 pane.getStyleClass().add(styleClass);
32 pane.setMinSize(w, h);
33 pane.setPrefSize(w, h);
34 pane.setMaxSize(w, h);
35 return pane;
36 }
37
38 public static Pane box(Node art, double w, double h, String styleClass) {
39 return initBox(new Pane(), art, w, h, styleClass);
40 }
41
42 public static double span(double p, double from, double to) {
43 return Math.max(0, Math.min(1, (p - from) / (to - from)));
44 }
45
46 public static double eased(double t) {
47 return Interpolator.EASE_BOTH.interpolate(0.0, 1.0, t);
48 }
49
50 public static double wrap(double p) {
51 return p < 0 ? p + 1 : p;
52 }
53
54 public static double at(double p, double... stops) {
55 for (int i = 2; i < stops.length; i += 2) {
56 if (p <= stops[i]) {
57 double width = stops[i] - stops[i - 2];
58 double t = width <= 0 ? 1 : (p - stops[i - 2]) / width;
59 return stops[i - 1] + t * (stops[i + 1] - stops[i - 1]);
60 }
61 }
62 return stops[stops.length - 1];
63 }
64}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChart.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChart.java
new file mode 100644
index 0000000..9de978c
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChart.java
@@ -0,0 +1,28 @@
1package market.guess.ui.desktop.components.graphic;
2
3import javafx.scene.layout.Pane;
4
5/**
6 * Custom declarative JavaFX component for the empty-state decorative animated market chart.
7 *
8 * <p>Can be declared directly in FXML:
9 *
10 * <pre>{@code
11 * <MarketChart fx:id="marketChart" />
12 * }</pre>
13 */
14public class MarketChart extends Pane {
15 private final Graphic graphic;
16
17 public MarketChart() {
18 this.graphic = MarketChartGraphic.build(this);
19 }
20
21 public void setPlaying(boolean playing) {
22 graphic.setPlaying(playing);
23 }
24
25 public Graphic getGraphic() {
26 return graphic;
27 }
28}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChartGraphic.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChartGraphic.java
new file mode 100644
index 0000000..1626f25
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChartGraphic.java
@@ -0,0 +1,121 @@
1package market.guess.ui.desktop.components.graphic;
2
3import static market.guess.ui.desktop.components.graphic.GraphicHelper.at;
4import static market.guess.ui.desktop.components.graphic.GraphicHelper.dot;
5import static market.guess.ui.desktop.components.graphic.GraphicHelper.eased;
6import static market.guess.ui.desktop.components.graphic.GraphicHelper.initBox;
7import static market.guess.ui.desktop.components.graphic.GraphicHelper.place;
8import static market.guess.ui.desktop.components.graphic.GraphicHelper.span;
9import static market.guess.ui.desktop.components.graphic.GraphicHelper.styled;
10import static market.guess.ui.desktop.components.graphic.GraphicHelper.wrap;
11
12import java.util.function.DoubleConsumer;
13import javafx.scene.Group;
14import javafx.scene.layout.Pane;
15import javafx.scene.shape.CubicCurveTo;
16import javafx.scene.shape.Line;
17import javafx.scene.shape.MoveTo;
18import javafx.scene.shape.Path;
19import javafx.scene.shape.StrokeLineCap;
20import javafx.scene.text.Text;
21import javafx.scene.transform.Scale;
22
23/**
24 * Empty-state market chart decorative graphic.
25 * Authored in 240x130 viewBox, then scaled to 300x162 display size.
26 */
27public final class MarketChartGraphic {
28 public static final double VIEW_SCALE = 1.25;
29
30 /** Cubic control net: p0, c1, c2, p3. Both curves leave the 0.50 midline together. */
31 public static final double[] YES_CURVE = {22, 66, 96, 64, 150, 22, 224, 22};
32 public static final double[] NO_CURVE = {22, 66, 96, 68, 150, 110, 224, 110};
33
34 private MarketChartGraphic() {}
35
36 public static Graphic create() {
37 return build(new Pane());
38 }
39
40 public static Graphic build(Pane pane) {
41 var yes = curve(YES_CURVE, "gm-anim-curve-yes");
42 var no = curve(NO_CURVE, "gm-anim-curve-no");
43 var yesDot = dot(4, "gm-anim-dot-yes");
44 var noDot = dot(4, "gm-anim-dot-no");
45 var tickHi = tickLabel("1.0", 24);
46 var tickLo = tickLabel("0.0", 114);
47
48 var art =
49 new Group(
50 rule(20, false),
51 rule(65, true),
52 rule(110, false),
53 styled(new Line(18, 14, 18, 116), "gm-anim-rule"),
54 tickHi,
55 tickLo,
56 yes,
57 no,
58 yesDot,
59 noDot);
60 art.getTransforms().add(new Scale(VIEW_SCALE, VIEW_SCALE));
61
62 DoubleConsumer render =
63 p -> {
64 double draw = eased(span(p, 0, .55));
65 yes.setStrokeDashOffset(300 * (1 - draw));
66 no.setStrokeDashOffset(300 * (1 - draw));
67 place(yesDot, rideCurve(YES_CURVE, draw));
68 place(noDot, rideCurve(NO_CURVE, draw));
69 double dotFade = at(p, 0, 0, .08, 1);
70 yesDot.setOpacity(dotFade);
71 noDot.setOpacity(dotFade);
72 tickHi.setOpacity(tickOpacity(p));
73 tickLo.setOpacity(tickOpacity(wrap(p - .11)));
74 };
75 initBox(pane, art, 300, 162, "gm-anim-market-chart");
76 return new Graphic(pane, render);
77 }
78
79 private static Path curve(double[] c, String styleClass) {
80 var path =
81 new Path(new MoveTo(c[0], c[1]), new CubicCurveTo(c[2], c[3], c[4], c[5], c[6], c[7]));
82 path.setFill(null);
83 path.setStrokeWidth(2.5);
84 path.setStrokeLineCap(StrokeLineCap.ROUND);
85 // Curve length is ~210, so a 300 dash covers the whole path with room to spare.
86 path.getStrokeDashArray().setAll(300.0, 300.0);
87 return styled(path, styleClass);
88 }
89
90 /**
91 * The cubic's {x, y} at parameter {@code t}.
92 */
93 public static double[] rideCurve(double[] c, double t) {
94 double u = 1 - t;
95 double b0 = u * u * u;
96 double b1 = 3 * u * u * t;
97 double b2 = 3 * u * t * t;
98 double b3 = t * t * t;
99 return new double[] {
100 b0 * c[0] + b1 * c[2] + b2 * c[4] + b3 * c[6], b0 * c[1] + b1 * c[3] + b2 * c[5] + b3 * c[7]
101 };
102 }
103
104 private static Line rule(double y, boolean dashed) {
105 var line = new Line(18, y, 228, y);
106 if (dashed) {
107 line.getStrokeDashArray().setAll(3.0, 4.0);
108 }
109 return styled(line, "gm-anim-rule");
110 }
111
112 private static Text tickLabel(String s, double baseline) {
113 var text = new Text(0, baseline, s);
114 return styled(text, "gm-anim-tick");
115 }
116
117 /** Axis labels breathe between 25% and 70% opacity. */
118 private static double tickOpacity(double p) {
119 return .25 + .45 * (1 - Math.abs(2 * p - 1));
120 }
121}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Motion.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Motion.java
new file mode 100644
index 0000000..4e8b6f2
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Motion.java
@@ -0,0 +1,59 @@
1package market.guess.ui.desktop.components.graphic;
2
3import javafx.scene.layout.Pane;
4import javafx.util.Duration;
5
6/**
7 * Coordinating entry point and facade for decorative animated graphics.
8 */
9public final class Motion {
10 public static final Duration CYCLE = Graphic.CYCLE;
11
12 public static final double[] YES_CURVE = MarketChartGraphic.YES_CURVE;
13 public static final double[] NO_CURVE = MarketChartGraphic.NO_CURVE;
14 public static final double[] SPARK = SparklineGraphic.SPARK;
15
16 private Motion() {}
17
18 // ---- Market Chart ------------------------------------------------------
19
20 public static Graphic marketChart() {
21 return MarketChartGraphic.create();
22 }
23
24 public static Graphic buildMarketChart(Pane pane) {
25 return MarketChartGraphic.build(pane);
26 }
27
28 public static double[] rideCurve(double[] c, double t) {
29 return MarketChartGraphic.rideCurve(c, t);
30 }
31
32 // ---- Equalizer ---------------------------------------------------------
33
34 public static Graphic equalizer() {
35 return EqualizerGraphic.create();
36 }
37
38 // ---- Sparkline ---------------------------------------------------------
39
40 public static Graphic sparkline() {
41 return SparklineGraphic.create();
42 }
43
44 public static double[] ridePolyline(double[] pts, double t) {
45 return SparklineGraphic.ridePolyline(pts, t);
46 }
47
48 // ---- Animation Toggle --------------------------------------------------
49
50 public static Graphic animationToggle() {
51 return AnimationToggleGraphic.create();
52 }
53
54 // ---- Interpolation -----------------------------------------------------
55
56 public static double at(double p, double... stops) {
57 return GraphicHelper.at(p, stops);
58 }
59}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/SparklineGraphic.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/SparklineGraphic.java
new file mode 100644
index 0000000..8816b54
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/SparklineGraphic.java
@@ -0,0 +1,65 @@
1package market.guess.ui.desktop.components.graphic;
2
3import static market.guess.ui.desktop.components.graphic.GraphicHelper.at;
4import static market.guess.ui.desktop.components.graphic.GraphicHelper.box;
5import static market.guess.ui.desktop.components.graphic.GraphicHelper.dot;
6import static market.guess.ui.desktop.components.graphic.GraphicHelper.eased;
7import static market.guess.ui.desktop.components.graphic.GraphicHelper.place;
8import static market.guess.ui.desktop.components.graphic.GraphicHelper.span;
9import static market.guess.ui.desktop.components.graphic.GraphicHelper.styled;
10
11import java.util.function.DoubleConsumer;
12import javafx.scene.Group;
13import javafx.scene.shape.Polyline;
14import javafx.scene.shape.StrokeLineCap;
15import javafx.scene.shape.StrokeLineJoin;
16
17/**
18 * Users tab balance sparkline graphic.
19 */
20public final class SparklineGraphic {
21 public static final double[] SPARK = {2, 20, 14, 16, 26, 22, 38, 11, 50, 14, 62, 5};
22
23 private SparklineGraphic() {}
24
25 public static Graphic create() {
26 var line = new Polyline(SPARK);
27 line.setFill(null);
28 line.setStrokeWidth(2);
29 line.setStrokeLineCap(StrokeLineCap.ROUND);
30 line.setStrokeLineJoin(StrokeLineJoin.ROUND);
31 line.getStrokeDashArray().setAll(120.0, 120.0);
32 styled(line, "gm-anim-spark");
33
34 var dot = dot(2.8, "gm-anim-spark-dot");
35
36 DoubleConsumer render =
37 p -> {
38 double draw = eased(span(p, 0, .70));
39 line.setStrokeDashOffset(120 * (1 - draw));
40 dot.setOpacity(at(p, 0, 0, .07, 1));
41 place(dot, ridePolyline(SPARK, draw));
42 };
43 return new Graphic(box(new Group(line, dot), 64, 28, "gm-anim-sparkline"), render);
44 }
45
46 /** The polyline's {x, y} at arc-length fraction {@code t}, so the dot sits on the drawn tip. */
47 public static double[] ridePolyline(double[] pts, double t) {
48 double total = 0;
49 for (int i = 2; i < pts.length; i += 2) {
50 total += Math.hypot(pts[i] - pts[i - 2], pts[i + 1] - pts[i - 1]);
51 }
52 double target = total * t;
53 for (int i = 2; i < pts.length; i += 2) {
54 double seg = Math.hypot(pts[i] - pts[i - 2], pts[i + 1] - pts[i - 1]);
55 if (target <= seg || i == pts.length - 2) {
56 double f = seg <= 0 ? 1 : Math.min(target / seg, 1);
57 return new double[] {
58 pts[i - 2] + f * (pts[i] - pts[i - 2]), pts[i - 1] + f * (pts[i + 1] - pts[i - 1])
59 };
60 }
61 target -= seg;
62 }
63 return new double[] {pts[0], pts[1]};
64 }
65}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/AppController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/AppController.java
new file mode 100644
index 0000000..c582051
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/AppController.java
@@ -0,0 +1,592 @@
1package market.guess.ui.desktop.controllers;
2
3import java.math.BigDecimal;
4import java.nio.file.Paths;
5import javafx.animation.FadeTransition;
6import javafx.animation.PauseTransition;
7import javafx.beans.binding.Bindings;
8import javafx.collections.ListChangeListener;
9import javafx.css.PseudoClass;
10import javafx.fxml.FXML;
11import javafx.scene.Node;
12import javafx.scene.control.Button;
13import javafx.scene.control.ComboBox;
14import javafx.scene.control.Label;
15import javafx.scene.control.TabPane;
16import javafx.scene.layout.BorderPane;
17import javafx.scene.layout.HBox;
18import javafx.scene.layout.Region;
19import javafx.scene.layout.StackPane;
20import javafx.stage.FileChooser;
21import javafx.stage.FileChooser.ExtensionFilter;
22import javafx.util.Duration;
23import market.guess.model.event.CreateEventRequest;
24import market.guess.model.event.EventStatus;
25import market.guess.model.event.MechanismType;
26import market.guess.ui.desktop.AppState;
27import market.guess.ui.desktop.AppView;
28import market.guess.ui.desktop.Skin;
29import market.guess.ui.desktop.components.AppTab;
30import market.guess.ui.desktop.components.ModalDialog;
31import market.guess.ui.desktop.components.graphic.AnimationToggleGraphic;
32import market.guess.ui.desktop.components.graphic.Graphic;
33import market.guess.ui.desktop.model.EventData;
34import market.guess.ui.desktop.model.UserData;
35import market.guess.ui.desktop.task.InitialLoadTask;
36import market.guess.ui.desktop.util.Format;
37import market.guess.ui.desktop.util.Views;
38
39public class AppController {
40 private static final PseudoClass SKIN_ROSE_PINE =
41 PseudoClass.getPseudoClass("skin-rose-pine-dawn");
42 private static final PseudoClass SKIN_CATPPUCCIN = PseudoClass.getPseudoClass("skin-catppuccin");
43 private static final PseudoClass SKIN_GRUVBOX = PseudoClass.getPseudoClass("skin-gruvbox");
44
45 private static final Duration TOAST_DURATION = Duration.millis(2500);
46 private static final Duration FADE_AWAY_DURATION = Duration.millis(500);
47 private AppView appView;
48 private double dragOffsetX;
49 private double dragOffsetY;
50 private InitialLoadTask currentLoadTask;
51 private Graphic animationsGraphic;
52
53 @FXML private StackPane rootStack;
54 @FXML private BorderPane chrome;
55 @FXML private HBox windowBar;
56 @FXML private TabPane tabPane;
57 @FXML private HBox tabInfo;
58 @FXML private Label loadedFilePathLabel;
59 @FXML private ComboBox<String> skinComboBox;
60 @FXML private Button animationsButton;
61 @FXML private Label balanceValueLabel;
62 @FXML private Label actingAsLabel;
63 @FXML private StackPane centerContainer;
64 @FXML private Label statusBarLabel;
65 @FXML private Button ejectButton;
66 @FXML private StackPane dialogContainer;
67 @FXML private StackPane toastContainer;
68 @FXML private Label toastLabel;
69
70 // Injected by FXMLLoader from app_view.fxml via <fx:include>
71 @FXML private Node emptyState;
72 @FXML private EmptyStateController emptyStateController;
73 @FXML private Node eventsTab;
74 @FXML private EventsTabController eventsTabController;
75 @FXML private Node usersTab;
76 @FXML private UsersTabController usersTabController;
77 @FXML private Node loadDialog;
78 @FXML private LoadDialogController loadDialogController;
79 @FXML private Node createEventDialog;
80 @FXML private CreateEventDialogController createEventDialogController;
81 @FXML private Node resolveDialog;
82 @FXML private ResolveDialogController resolveDialogController;
83
84 private Node currentlyActiveContent;
85
86 @FXML
87 private void initialize() {
88 loadDialogController.setOnCancel(this::cancelLoad);
89 createEventDialogController.setOnCancel(this::closeDialog);
90 createEventDialogController.setOnCreate((CreateEventRequest req) -> createEvent(req));
91 resolveDialogController.setOnCancel(this::closeDialog);
92 resolveDialogController.setOnConfirm(this::resolveEvent);
93 }
94
95 public void init(AppView appView) {
96 this.appView = appView;
97 setupWindowDrag();
98 setupSkinComboBox();
99 setupAnimationsButton();
100 initSubViews();
101 setupStateBindings();
102 applyInitialState();
103 }
104
105 private void setupAnimationsButton() {
106 animationsGraphic = AnimationToggleGraphic.create();
107 if (animationsButton != null) {
108 animationsButton.setGraphic(animationsGraphic.getNode());
109 }
110 }
111
112 private void setupWindowDrag() {
113 windowBar.setOnMousePressed(
114 e -> {
115 dragOffsetX = e.getSceneX();
116 dragOffsetY = e.getSceneY();
117 });
118 windowBar.setOnMouseDragged(
119 e -> {
120 appView.getStage().setX(e.getScreenX() - dragOffsetX);
121 appView.getStage().setY(e.getScreenY() - dragOffsetY);
122 });
123 windowBar.setOnMouseClicked(
124 e -> {
125 if (e.getClickCount() == 2) {
126 var stage = appView.getStage();
127 stage.setMaximized(!stage.isMaximized());
128 }
129 });
130 }
131
132 private void setupSkinComboBox() {
133 skinComboBox.getItems().setAll("Rosé Pine Dawn", "Catppuccin", "Gruvbox");
134 skinComboBox.setValue("Rosé Pine Dawn");
135 }
136
137 private void initSubViews() {
138 var state = getState();
139 eventsTabController.init(state);
140 eventsTabController.setOnNewEvent(this::openCreateEventDialog);
141 eventsTabController.setOnOpenEvent(this::openEvent);
142 eventsTabController.setOnCloseEvent(this::openResolveDialog);
143
144 usersTabController.init(state);
145 usersTabController.setOnCreateNewEvent(this::openCreateEventDialog);
146 usersTabController.setOnToast(this::toast);
147 usersTabController.setOnPlaceOrder(this::placeOrder);
148 }
149
150 private void setupStateBindings() {
151 var state = appView.getState();
152
153 // Declarative property bindings
154 loadedFilePathLabel
155 .textProperty()
156 .bind(
157 Bindings.when(
158 state.loadedFileProperty().isNull().or(state.loadedFileProperty().isEmpty()))
159 .then("")
160 .otherwise(state.loadedFileProperty()));
161
162 ejectButton.visibleProperty().bind(state.loadedFileProperty().isNotNull());
163 ejectButton.managedProperty().bind(ejectButton.visibleProperty());
164
165 tabInfo.visibleProperty().bind(tabPane.visibleProperty());
166 // The header area only exists once the skin is installed; match its height so the
167 // overlaid acting-as/balance labels stay vertically centred in it.
168 tabPane
169 .skinProperty()
170 .addListener(
171 (obs, oldV, newV) -> {
172 var header = (Region) tabPane.lookup(".tab-header-area");
173 tabInfo.prefHeightProperty().bind(header.heightProperty());
174 });
175 tabPane
176 .getSelectionModel()
177 .selectedIndexProperty()
178 .addListener((obs, oldV, newV) -> state.setActiveTab(AppTab.values()[newV.intValue()]));
179
180 // Granular reactive listeners
181 state.skinProperty().addListener((obs, oldV, newV) -> updateSkin(newV));
182 state
183 .loadedFileProperty()
184 .addListener(
185 (obs, oldV, newV) -> {
186 updateCenterContent();
187 updateStatusBar();
188 });
189 state.activeTabProperty().addListener((obs, oldV, newV) -> updateTabs(newV));
190 state.dialogProperty().addListener((obs, oldV, newV) -> updateDialogOverlay());
191 state.toastProperty().addListener((obs, oldV, newV) -> updateToastOverlay());
192 state.animationsOnProperty().addListener((obs, oldV, newV) -> updateAnimations(newV));
193 state.actingUserProperty().addListener((obs, oldV, newV) -> updateActingUserDisplay());
194 state.actingUserNameProperty().addListener((obs, oldV, newV) -> updateActingUserDisplay());
195
196 state.getEvents().addListener((ListChangeListener<EventData>) c -> updateStatusBar());
197 state.getUsers().addListener((ListChangeListener<UserData>) c -> updateStatusBar());
198 }
199
200 public AppState getState() {
201 return appView != null ? appView.getState() : null;
202 }
203
204 /**
205 * Refreshes UI state across all components. Kept for lifecycle compatibility; delegates to
206 * targeted update methods.
207 */
208 public void refresh() {
209 applyInitialState();
210 }
211
212 private void applyInitialState() {
213 var state = getState();
214 if (state == null) return;
215
216 updateSkin(state.getSkin());
217 updateAnimations(state.isAnimationsOn());
218 updateTabs(state.getActiveTab());
219 updateActingUserDisplay();
220 updateCenterContent();
221 updateStatusBar();
222 updateDialogOverlay();
223 updateToastOverlay();
224 }
225
226 private void updateSkin(Skin skin) {
227 if (skin == null) return;
228 rootStack.pseudoClassStateChanged(SKIN_ROSE_PINE, skin == Skin.ROSE_PINE_DAWN);
229 rootStack.pseudoClassStateChanged(SKIN_CATPPUCCIN, skin == Skin.CATPPUCCIN);
230 rootStack.pseudoClassStateChanged(SKIN_GRUVBOX, skin == Skin.GRUVBOX);
231
232 String comboVal =
233 switch (skin) {
234 case CATPPUCCIN -> "Catppuccin";
235 case GRUVBOX -> "Gruvbox";
236 default -> "Rosé Pine Dawn";
237 };
238 if (!comboVal.equals(skinComboBox.getValue())) {
239 skinComboBox.setValue(comboVal);
240 }
241 }
242
243 private void updateAnimations(boolean animOn) {
244 Views.setSelected(animationsButton, animOn);
245 if (animationsGraphic != null) {
246 animationsGraphic.setPlaying(animOn);
247 }
248 if (emptyStateController != null) {
249 emptyStateController.setAnimating(animOn);
250 }
251 }
252
253 private void updateTabs(AppTab tab) {
254 // ponytail: tab order in app_view.fxml must match AppTab declaration order
255 tabPane.getSelectionModel().select(tab.ordinal());
256 updateCenterContent();
257 }
258
259 private void updateActingUserDisplay() {
260 var state = getState();
261 if (state == null) return;
262 var actor = state.getActingUser();
263 if (actor == null) return;
264 balanceValueLabel.setText(Format.money(actor.balance));
265 actingAsLabel.setText(
266 "acting as " + (state.getActingUserName() == null ? "" : state.getActingUserName()));
267 }
268
269 private void updateCenterContent() {
270 var state = getState();
271 if (state == null) return;
272
273 boolean showEmpty = (state.getLoadedFile() == null);
274 boolean showEvents = !showEmpty && (state.getActiveTab() == AppTab.EVENTS);
275 boolean showUsers = !showEmpty && (state.getActiveTab() == AppTab.USERS);
276
277 Node activeContent = showEmpty ? emptyState : (showEvents ? eventsTab : usersTab);
278
279 emptyState.setVisible(showEmpty);
280 emptyState.setManaged(showEmpty);
281
282 tabPane.setVisible(!showEmpty);
283 tabPane.setManaged(!showEmpty);
284
285 if (showEvents && eventsTabController != null) {
286 eventsTabController.refresh();
287 }
288 if (showUsers && usersTabController != null) {
289 usersTabController.refresh();
290 }
291
292 if (currentlyActiveContent != activeContent) {
293 Node prev = currentlyActiveContent;
294 currentlyActiveContent = activeContent;
295 if (state.isAnimationsOn() && state.getLoadedFile() != null && prev != null) {
296 var fade = new FadeTransition(FADE_AWAY_DURATION, activeContent);
297 fade.setFromValue(0);
298 fade.setToValue(1);
299 fade.play();
300 }
301 }
302 }
303
304 private void updateStatusBar() {
305 var state = getState();
306 if (state == null || statusBarLabel == null) return;
307
308 if (state.getLoadedFile() == null) {
309 statusBarLabel.setText(" IDLE  ");
310 } else {
311 String fileName = Paths.get(state.getLoadedFile()).getFileName().toString();
312 statusBarLabel.setText(
313 " ï…› "
314 + fileName
315 + "   "
316 + state.getEvents().size()
317 + " events   "
318 + state.getUsers().size()
319 + " users");
320 }
321 }
322
323 private void updateDialogOverlay() {
324 var state = getState();
325 if (state == null) return;
326
327 boolean showDialog = (state.getDialog() != ModalDialog.NONE);
328 dialogContainer.setVisible(showDialog);
329 dialogContainer.setMouseTransparent(!showDialog);
330
331 boolean isLoad = (state.getDialog() == ModalDialog.LOAD);
332 loadDialog.setVisible(isLoad);
333 loadDialog.setManaged(isLoad);
334
335 boolean isCreate = (state.getDialog() == ModalDialog.CREATE_EVENT);
336 createEventDialog.setVisible(isCreate);
337 createEventDialog.setManaged(isCreate);
338
339 boolean isResolve = (state.getDialog() == ModalDialog.RESOLVE);
340 resolveDialog.setVisible(isResolve);
341 resolveDialog.setManaged(isResolve);
342 }
343
344 private void updateToastOverlay() {
345 var state = getState();
346 if (state == null) return;
347
348 if (state.getToast() != null) {
349 toastLabel.setText(state.getToast());
350 toastLabel.setVisible(true);
351 } else {
352 toastLabel.setVisible(false);
353 }
354 }
355
356 // ---- FXML action handlers ----------------------------------------------
357
358 @FXML
359 private void handleClose() {
360 appView.getStage().close();
361 }
362
363 @FXML
364 private void handleMinimize() {
365 appView.getStage().setIconified(true);
366 }
367
368 @FXML
369 private void handleMaximize() {
370 var stage = appView.getStage();
371 stage.setMaximized(!stage.isMaximized());
372 }
373
374 @FXML
375 private void handleSkinChanged() {
376 String val = skinComboBox.getValue();
377 if (val == null) return;
378 Skin skin =
379 switch (val) {
380 case "Catppuccin" -> Skin.CATPPUCCIN;
381 case "Gruvbox" -> Skin.GRUVBOX;
382 default -> Skin.ROSE_PINE_DAWN;
383 };
384 var state = appView.getState();
385 if (state != null && state.getSkin() != skin) {
386 state.setSkin(skin);
387 }
388 }
389
390 @FXML
391 private void handleAnimationsToggled() {
392 appView.getState().setAnimationsOn(!appView.getState().isAnimationsOn());
393 }
394
395 @FXML
396 private void handleLoadFile() {
397 startLoad();
398 }
399
400 @FXML
401 private void handleEjectFile() {
402 appView.getState().setLoadedFile(null);
403 }
404
405 // ---- Public Actions ---------------------------------------------------
406
407 public void toast(String msg) {
408 appView.getState().setToast(msg);
409 var pause = new PauseTransition(TOAST_DURATION);
410 pause.setOnFinished(e -> appView.getState().setToast(null));
411 pause.play();
412 }
413
414 public void startLoad() {
415 var stage = appView.getStage();
416 var fc = new FileChooser();
417 fc.setTitle("Load events file");
418 fc.getExtensionFilters().add(new ExtensionFilter("GuessMarket Files", "*.xml"));
419 var picked = stage.getScene() == null ? null : fc.showOpenDialog(stage);
420 if (picked == null) {
421 return;
422 }
423 var state = appView.getState();
424 state.setPendingFile(picked.getAbsolutePath());
425
426 var loadTask =
427 new InitialLoadTask(appView.getCatalogContext(), Paths.get(state.getPendingFile()));
428 this.currentLoadTask = loadTask;
429
430 loadTask.setOnRunning(
431 e -> {
432 state.setDialog(ModalDialog.LOAD);
433 loadDialogController.show(loadTask, state.getPendingFile());
434 });
435
436 loadTask.setOnCancelled(
437 e -> {
438 this.currentLoadTask = null;
439 loadDialogController.reset();
440 state.setDialog(ModalDialog.NONE);
441 });
442
443 loadTask.setOnFailed(
444 e -> {
445 this.currentLoadTask = null;
446 loadDialogController.reset();
447 cancelLoad();
448 Throwable ex = loadTask.getException();
449 String msg =
450 (ex != null && ex.getMessage() != null && !ex.getMessage().isBlank())
451 ? ex.getMessage()
452 : "Failed to load file.";
453 toast(msg);
454 });
455
456 loadTask.setOnSucceeded(
457 e -> {
458 this.currentLoadTask = null;
459 loadDialogController.reset();
460 if (state.getDialog() != ModalDialog.LOAD) return;
461 var loadResult = loadTask.getValue();
462 state.setLoadedFile(loadResult.source());
463 state.refreshData();
464 state.setDialog(ModalDialog.NONE);
465 toast(
466 "File loaded  "
467 + loadResult.eventsLoaded()
468 + " events, "
469 + state.getUsers().size()
470 + " users");
471 });
472
473 var thread = new Thread(loadTask);
474 thread.setDaemon(true);
475 thread.start();
476 }
477
478 public void cancelLoad() {
479 if (currentLoadTask != null) {
480 currentLoadTask.cancel();
481 currentLoadTask = null;
482 }
483 loadDialogController.reset();
484 appView.getState().setDialog(ModalDialog.NONE);
485 }
486
487 public void openCreateEventDialog() {
488 var state = appView.getState();
489 state.setCreateType(MechanismType.LMSR);
490 createEventDialogController.reset();
491 state.setDialog(ModalDialog.CREATE_EVENT);
492 }
493
494 public void closeDialog() {
495 appView.getState().setDialog(ModalDialog.NONE);
496 }
497
498 public void openResolveDialog() {
499 var state = appView.getState();
500 state.setResolveSelectedOption(null);
501 resolveDialogController.reset();
502 state.setDialog(ModalDialog.RESOLVE);
503 }
504
505 public void createEvent(CreateEventRequest req) {
506 var state = appView.getState();
507 state.setCreateType(req.mechanism());
508 var requestWithMM =
509 new CreateEventRequest(
510 req.name(),
511 req.description(),
512 req.mechanism(),
513 req.commissionPercent(),
514 req.commissionTiming(),
515 req.mmUserName() != null ? req.mmUserName() : state.getActingUserName(),
516 req.liquidityB(),
517 req.baseValueD(),
518 req.options(),
519 req.allowMinting());
520 var res = appView.getCatalogContext().createEvent(requestWithMM);
521 if (res.isSuccess()) {
522 state.refreshData();
523 state.setDialog(ModalDialog.NONE);
524 state.setSelectedEventId(res.getData().summary().key());
525 toast("Event created — you are its market maker");
526 } else {
527 toast("Failed to create event: " + res.getMessage());
528 }
529 }
530
531 public void resolveEvent(String option) {
532 var state = appView.getState();
533 var e = state.getSelectedEvent();
534 if (e == null) return;
535 var res = appView.getMarketContext().settleEvent(state.getActingUserName(), e.id, option);
536 if (res.isSuccess()) {
537 state.refreshData();
538 state.setDialog(ModalDialog.NONE);
539 toast("Event resolved as " + option);
540 } else {
541 toast("Failed to resolve event: " + res.getMessage());
542 }
543 }
544
545 public void openEvent(EventData e) {
546 var state = appView.getState();
547 if (e.status == EventStatus.DRAFT) {
548 var res = appView.getCatalogContext().openEvent(e.id);
549 if (!res.isSuccess()) {
550 toast("Failed to open event: " + res.getMessage());
551 return;
552 }
553 state.refreshData();
554 }
555 // The trade panel lives on the Users tab and follows the selected event.
556 state.setSelectedEventId(e.id);
557 state.setActiveTab(AppTab.USERS);
558 toast("Trading " + e.name + " as " + state.getActingUserName());
559 }
560
561 public void placeOrder(String eventKey, String optionKey, BigDecimal price, long quantity) {
562 placeOrder(eventKey, optionKey, "BUY", price, quantity);
563 }
564
565 public void placeOrder(
566 String eventKey, String optionKey, String side, BigDecimal price, long quantity) {
567 var state = appView.getState();
568 var res =
569 appView
570 .getMarketContext()
571 .placeOrder(state.getActingUserName(), eventKey, optionKey, side, price, quantity);
572 if (res.isSuccess()) {
573 state.refreshData();
574 var receipt = res.getData();
575 boolean isBuy = "BUY".equalsIgnoreCase(side);
576 if (receipt.trades() == null || receipt.trades().isEmpty()) {
577 toast("Resting " + side + " order placed: " + quantity + " @ " + Format.money(price));
578 } else {
579 toast(
580 "Order placed  "
581 + (isBuy ? "Paid " : "Received ")
582 + Format.money(new BigDecimal(receipt.totalPaid()))
583 + " for "
584 + quantity
585 + " "
586 + optionKey);
587 }
588 } else {
589 toast("Trade failed: " + res.getMessage());
590 }
591 }
592}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/CreateEventDialogController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/CreateEventDialogController.java
new file mode 100644
index 0000000..5649145
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/CreateEventDialogController.java
@@ -0,0 +1,163 @@
1package market.guess.ui.desktop.controllers;
2
3import java.math.BigDecimal;
4import java.util.List;
5import java.util.function.BiConsumer;
6import java.util.function.Consumer;
7import javafx.fxml.FXML;
8import javafx.scene.control.Button;
9import javafx.scene.control.CheckBox;
10import javafx.scene.control.ComboBox;
11import javafx.scene.control.Label;
12import javafx.scene.control.TextArea;
13import javafx.scene.control.TextField;
14import market.guess.model.event.CommissionTiming;
15import market.guess.model.event.CreateEventRequest;
16import market.guess.model.event.MechanismType;
17import market.guess.ui.desktop.util.Format;
18import market.guess.ui.desktop.util.Views;
19
20public class CreateEventDialogController {
21 private Runnable onCancel;
22 private Consumer<CreateEventRequest> onCreateRequest;
23 private BiConsumer<String, MechanismType> onCreate;
24 private MechanismType selectedType = MechanismType.LMSR;
25
26 @FXML private TextField nameField;
27 @FXML private TextArea descArea;
28 @FXML private Button lmsrBtn;
29 @FXML private Button orderBookBtn;
30 @FXML private ComboBox<String> feeCollectionCombo;
31 @FXML private TextField feePercentField;
32 @FXML private Label paramLabel;
33 @FXML private TextField paramField;
34 @FXML private TextField opt1Field;
35 @FXML private TextField opt2Field;
36 @FXML private CheckBox mintingCheckBox;
37 @FXML private Label infoNoteLabel;
38
39 @FXML
40 private void initialize() {
41 feeCollectionCombo.getItems().setAll("On purchase", "On close");
42 feeCollectionCombo.setValue("On purchase");
43 updateTypeSelection();
44 }
45
46 public void setOnCancel(Runnable onCancel) {
47 this.onCancel = onCancel;
48 }
49
50 public void setOnCreate(Consumer<CreateEventRequest> onCreateRequest) {
51 this.onCreateRequest = onCreateRequest;
52 }
53
54 public void setOnCreate(BiConsumer<String, MechanismType> onCreate) {
55 this.onCreate = onCreate;
56 }
57
58 public void reset() {
59 nameField.setText("Will the summit happen before Q4?");
60 descArea.setText(
61 "Resolves YES if a joint summit is publicly confirmed and held before October 1st.");
62 feeCollectionCombo.setValue("On purchase");
63 feePercentField.setText("5");
64 opt1Field.setText("YES");
65 opt2Field.setText("NO");
66 selectedType = MechanismType.LMSR;
67 updateTypeSelection();
68 }
69
70 private void updateTypeSelection() {
71 Views.setSelected(lmsrBtn, selectedType == MechanismType.LMSR);
72 Views.setSelected(orderBookBtn, selectedType == MechanismType.ORDER_BOOK);
73
74 boolean isLmsr = selectedType == MechanismType.LMSR;
75 paramLabel.setText(isLmsr ? "Liquidity b (integer)" : "Base value d ($)");
76 paramField.setText(isLmsr ? "100" : "1.00");
77 mintingCheckBox.setVisible(!isLmsr);
78
79 String infoText =
80 isLmsr
81 ? "Opening this event will move "
82 + Format.money(BigDecimal.valueOf(100 * Math.log(2)))
83 + " of subsidy from your account into the contract account (b  ln 2)."
84 : "Opening this event will buy the initial share inventory from your account into the"
85 + " contract account.";
86 infoNoteLabel.setText(infoText);
87 }
88
89 @FXML
90 private void handleSelectLmsr() {
91 selectedType = MechanismType.LMSR;
92 updateTypeSelection();
93 }
94
95 @FXML
96 private void handleSelectOrderBook() {
97 selectedType = MechanismType.ORDER_BOOK;
98 updateTypeSelection();
99 }
100
101 @FXML
102 private void handleCancel() {
103 if (onCancel != null) {
104 onCancel.run();
105 }
106 }
107
108 @FXML
109 private void handleCreate() {
110 String name = nameField.getText() != null ? nameField.getText().trim() : "";
111 String desc = descArea.getText() != null ? descArea.getText().trim() : "";
112 MechanismType type = selectedType;
113 CommissionTiming timing = "On close".equalsIgnoreCase(feeCollectionCombo.getValue())
114 ? CommissionTiming.ON_CLOSE
115 : CommissionTiming.ON_PURCHASE;
116
117 int fee = 5;
118 try {
119 fee = Integer.parseInt(feePercentField.getText().trim());
120 if (fee < 0) fee = 0;
121 if (fee > 90) fee = 90;
122 } catch (Exception ignored) {
123 }
124
125 Integer b = null;
126 BigDecimal d = null;
127 if (type == MechanismType.LMSR) {
128 try {
129 b = Integer.parseInt(paramField.getText().trim());
130 } catch (Exception ignored) {
131 b = 100;
132 }
133 } else {
134 try {
135 d = new BigDecimal(paramField.getText().trim());
136 } catch (Exception ignored) {
137 d = BigDecimal.ONE;
138 }
139 }
140
141 String opt1 = opt1Field.getText() != null && !opt1Field.getText().isBlank() ? opt1Field.getText().trim() : "YES";
142 String opt2 = opt2Field.getText() != null && !opt2Field.getText().isBlank() ? opt2Field.getText().trim() : "NO";
143 boolean minting = mintingCheckBox.isSelected();
144
145 var request = new CreateEventRequest(
146 name,
147 desc,
148 type,
149 fee,
150 timing,
151 null,
152 b,
153 d,
154 List.of(opt1, opt2),
155 minting);
156
157 if (onCreateRequest != null) {
158 onCreateRequest.accept(request);
159 } else if (onCreate != null) {
160 onCreate.accept(name, type);
161 }
162 }
163}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EmptyStateController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EmptyStateController.java
new file mode 100644
index 0000000..9b01095
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EmptyStateController.java
@@ -0,0 +1,14 @@
1package market.guess.ui.desktop.controllers;
2
3import javafx.fxml.FXML;
4import market.guess.ui.desktop.components.graphic.MarketChart;
5
6public class EmptyStateController {
7 @FXML private MarketChart marketChart;
8
9 public void setAnimating(boolean animating) {
10 if (marketChart != null) {
11 marketChart.setPlaying(animating);
12 }
13 }
14}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EventsTabController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EventsTabController.java
new file mode 100644
index 0000000..e71be40
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EventsTabController.java
@@ -0,0 +1,496 @@
1package market.guess.ui.desktop.controllers;
2
3import java.util.ArrayList;
4import java.util.function.Consumer;
5import javafx.collections.ListChangeListener;
6import javafx.css.PseudoClass;
7import javafx.fxml.FXML;
8import javafx.scene.Node;
9import javafx.scene.chart.LineChart;
10import javafx.scene.control.Button;
11import javafx.scene.control.Label;
12import javafx.scene.control.ListView;
13import javafx.scene.control.ProgressBar;
14import javafx.scene.layout.HBox;
15import javafx.scene.layout.StackPane;
16import javafx.scene.layout.VBox;
17import market.guess.model.event.CommissionTiming;
18import market.guess.model.event.EventStatus;
19import market.guess.model.event.MechanismType;
20import market.guess.ui.desktop.AppState;
21import market.guess.ui.desktop.components.CommissionFilter;
22import market.guess.ui.desktop.components.EventListCell;
23import market.guess.ui.desktop.components.LadderItemView;
24import market.guess.ui.desktop.components.MechanismFilter;
25import market.guess.ui.desktop.components.ParticipantItemView;
26import market.guess.ui.desktop.components.TradeHistoryItemView;
27import market.guess.ui.desktop.components.graphic.EqualizerGraphic;
28import market.guess.ui.desktop.components.graphic.Graphic;
29import market.guess.ui.desktop.model.ChartPoint;
30import market.guess.ui.desktop.model.EventData;
31import market.guess.ui.desktop.model.OrderBook;
32import market.guess.ui.desktop.model.ParticipantRow;
33import market.guess.ui.desktop.model.TradeRow;
34import market.guess.ui.desktop.util.Charts;
35import market.guess.ui.desktop.util.Format;
36import market.guess.ui.desktop.util.Views;
37
38public class EventsTabController {
39 private static final PseudoClass ACTIVE = PseudoClass.getPseudoClass("active");
40 private static final PseudoClass CLOSED = PseudoClass.getPseudoClass("closed");
41 private static final PseudoClass IDLE = PseudoClass.getPseudoClass("idle");
42
43 private AppState state;
44 private Runnable onNewEvent;
45 private Consumer<EventData> onOpenEvent;
46 private Runnable onCloseEvent;
47 private Graphic equalizer;
48 private boolean updatingSelection = false;
49
50 @FXML private Button methodFilterAllBtn;
51 @FXML private Button methodFilterLmsrBtn;
52 @FXML private Button methodFilterObBtn;
53 @FXML private Button statusFilterAllBtn;
54 @FXML private Button statusFilterNotStartedBtn;
55 @FXML private Button statusFilterActiveBtn;
56 @FXML private Button statusFilterClosedBtn;
57 @FXML private Button feeFilterAllBtn;
58 @FXML private Button feeFilterPurchaseBtn;
59 @FXML private Button feeFilterCloseBtn;
60
61 @FXML private ListView<EventData> eventListContainer;
62 @FXML private Label eventCountLabel;
63
64 @FXML private VBox eventDetailCard;
65 @FXML private Label eventNameLabel;
66 @FXML private Label eventDescLabel;
67 @FXML private Button openEventBtn;
68 @FXML private Button closeEventBtn;
69
70 @FXML private Label metaMethodLabel;
71 @FXML private Label metaStatusLabel;
72 @FXML private HBox livePill;
73 @FXML private StackPane liveBarsBox;
74 @FXML private Label liveLabel;
75 @FXML private Label metaMmLabel;
76 @FXML private Label metaFeeLabel;
77 @FXML private Label metaContractLabel;
78
79 @FXML private VBox lmsrSection;
80 @FXML private Label lmsrYesPriceLabel;
81 @FXML private ProgressBar lmsrYesProgressBar;
82 @FXML private Label lmsrYesSharesLabel;
83 @FXML private Label lmsrNoPriceLabel;
84 @FXML private ProgressBar lmsrNoProgressBar;
85 @FXML private Label lmsrNoSharesLabel;
86 @FXML private Label lmsrChartSubLabel;
87 @FXML private LineChart<Number, Number> lmsrChart;
88 @FXML private VBox tradeHistoryRowsContainer;
89
90 @FXML private VBox orderBookSection;
91 @FXML private Label yesBookCountLabel;
92 @FXML private Label yesLastLabel;
93 @FXML private Label yesMidLabel;
94 @FXML private Label yesBestBidLabel;
95 @FXML private Label yesBestAskLabel;
96 @FXML private Label yesSpreadLabel;
97 @FXML private VBox yesBookLadderList;
98
99 @FXML private Label noBookCountLabel;
100 @FXML private Label noLastLabel;
101 @FXML private Label noMidLabel;
102 @FXML private Label noBestBidLabel;
103 @FXML private Label noBestAskLabel;
104 @FXML private Label noSpreadLabel;
105 @FXML private VBox noBookLadderList;
106
107 @FXML private Label obChartSubLabel;
108 @FXML private LineChart<Number, Number> obChart;
109
110 @FXML private VBox participantsRowsContainer;
111 @FXML private HBox resolvedBanner;
112 @FXML private Label resolvedBannerText;
113
114 public void init(AppState state) {
115 this.state = state;
116 equalizer = EqualizerGraphic.create();
117 liveBarsBox.getChildren().setAll(equalizer.getNode());
118
119 eventListContainer.setCellFactory(lv -> new EventListCell());
120 eventListContainer
121 .getSelectionModel()
122 .selectedItemProperty()
123 .addListener(
124 (obs, oldVal, newVal) -> {
125 if (updatingSelection) return;
126 if (newVal != null && state != null) {
127 state.setSelectedEvent(newVal);
128 }
129 });
130
131 state
132 .selectedEventProperty()
133 .addListener(
134 (obs, oldVal, newVal) -> {
135 if (newVal != null) {
136 if (!newVal.equals(eventListContainer.getSelectionModel().getSelectedItem())) {
137 updatingSelection = true;
138 try {
139 eventListContainer.getSelectionModel().select(newVal);
140 } finally {
141 updatingSelection = false;
142 }
143 }
144 }
145 refreshDetailCard(newVal);
146 });
147
148 state.filterMethodProperty().addListener((obs, oldVal, newVal) -> refreshListAndFilters());
149 state.filterStatusProperty().addListener((obs, oldVal, newVal) -> refreshListAndFilters());
150 state.filterFeeProperty().addListener((obs, oldVal, newVal) -> refreshListAndFilters());
151 state
152 .getEvents()
153 .addListener(
154 (ListChangeListener<EventData>)
155 c -> {
156 refreshListAndFilters();
157 refreshDetailCard(state.selectedEvent());
158 });
159 state
160 .animationsOnProperty()
161 .addListener(
162 (obs, oldVal, newVal) -> {
163 var sel = state.selectedEvent();
164 boolean isActive = sel != null && sel.status == EventStatus.ACTIVE;
165 equalizer.setPlaying(newVal && isActive);
166 });
167 }
168
169 public void setOnNewEvent(Runnable onNewEvent) {
170 this.onNewEvent = onNewEvent;
171 }
172
173 public void setOnOpenEvent(Consumer<EventData> onOpenEvent) {
174 this.onOpenEvent = onOpenEvent;
175 }
176
177 public void setOnCloseEvent(Runnable onCloseEvent) {
178 this.onCloseEvent = onCloseEvent;
179 }
180
181 public void refresh() {
182 if (state == null) return;
183 refreshListAndFilters();
184 refreshDetailCard(state.selectedEvent());
185 }
186
187 private void refreshListAndFilters() {
188 if (state == null) return;
189
190 // 1. Update filter button selections
191 Views.setSelected(methodFilterAllBtn, state.getFilterMethod() == MechanismFilter.ALL);
192 Views.setSelected(methodFilterLmsrBtn, state.getFilterMethod() == MechanismFilter.LMSR);
193 Views.setSelected(methodFilterObBtn, state.getFilterMethod() == MechanismFilter.ORDER_BOOK);
194
195 Views.setSelected(statusFilterAllBtn, state.getFilterStatus() == null);
196 Views.setSelected(statusFilterNotStartedBtn, state.getFilterStatus() == EventStatus.DRAFT);
197 Views.setSelected(statusFilterActiveBtn, state.getFilterStatus() == EventStatus.ACTIVE);
198 Views.setSelected(statusFilterClosedBtn, state.getFilterStatus() == EventStatus.SETTLED);
199
200 Views.setSelected(feeFilterAllBtn, state.getFilterFee() == CommissionFilter.ALL);
201 Views.setSelected(feeFilterPurchaseBtn, state.getFilterFee() == CommissionFilter.PURCHASE);
202 Views.setSelected(feeFilterCloseBtn, state.getFilterFee() == CommissionFilter.CLOSE);
203
204 // 2. Update list
205 var filtered =
206 state.getEvents().stream()
207 .filter(
208 e ->
209 state.getFilterMethod() == MechanismFilter.ALL
210 || (state.getFilterMethod() == MechanismFilter.LMSR
211 && e.type == MechanismType.LMSR)
212 || (state.getFilterMethod() == MechanismFilter.ORDER_BOOK
213 && e.type == MechanismType.ORDER_BOOK))
214 .filter(e -> state.getFilterStatus() == null || e.status == state.getFilterStatus())
215 .filter(
216 e ->
217 state.getFilterFee() == CommissionFilter.ALL
218 || (state.getFilterFee() == CommissionFilter.PURCHASE
219 && e.feeMode == CommissionTiming.ON_PURCHASE)
220 || (state.getFilterFee() == CommissionFilter.CLOSE
221 && e.feeMode == CommissionTiming.ON_CLOSE))
222 .toList();
223
224 updatingSelection = true;
225 try {
226 eventListContainer.getItems().setAll(filtered);
227 eventCountLabel.setText(
228 filtered.size() + " of " + state.getEvents().size() + " events shown");
229
230 var sel = state.selectedEvent();
231 EventData match = null;
232 if (sel != null) {
233 for (var e : filtered) {
234 if (e.id.equals(sel.id)) {
235 match = e;
236 break;
237 }
238 }
239 }
240 if (match != null) {
241 eventListContainer.getSelectionModel().select(match);
242 } else if (!filtered.isEmpty()) {
243 eventListContainer.getSelectionModel().select(0);
244 }
245 } finally {
246 updatingSelection = false;
247 }
248 }
249
250 private void refreshDetailCard(EventData selected) {
251 if (selected == null) {
252 eventDetailCard.setVisible(false);
253 return;
254 }
255 eventDetailCard.setVisible(true);
256
257 eventNameLabel.setText(selected.num + ". " + selected.name);
258 eventDescLabel.setText(selected.desc);
259
260 // Open = go trade: any unblocked user on an ACTIVE event, or the MM activating their DRAFT.
261 var actor = state.getActingUser();
262 boolean isMm = selected.mm.equals(state.getActingUserName());
263 boolean canOpen =
264 actor != null
265 && !actor.blocked
266 && (selected.status == EventStatus.ACTIVE
267 || (selected.status == EventStatus.DRAFT && isMm));
268 boolean canClose =
269 selected.status == EventStatus.ACTIVE && selected.mm.equals(state.getActingUserName());
270
271 openEventBtn.setText(selected.status == EventStatus.DRAFT ? "Start Event" : "Trade");
272 openEventBtn.setVisible(canOpen);
273 openEventBtn.setManaged(canOpen);
274 closeEventBtn.setVisible(canClose);
275 closeEventBtn.setManaged(canClose);
276
277 // Meta fields
278 metaMethodLabel.setText(selected.type == MechanismType.LMSR ? "LMSR" : "Order Book");
279 metaStatusLabel.setText(selected.status.name());
280
281 // LIVE pill: only an ACTIVE market needs to look like order flow is arriving.
282 boolean isActive = selected.status == EventStatus.ACTIVE;
283 boolean isEnded = selected.status == EventStatus.SETTLED;
284 liveLabel.setText(isActive ? "LIVE" : isEnded ? "ENDED" : "IDLE");
285 livePill.pseudoClassStateChanged(ACTIVE, isActive);
286 livePill.pseudoClassStateChanged(CLOSED, isEnded);
287 livePill.pseudoClassStateChanged(IDLE, !isActive && !isEnded);
288 equalizer.setPlaying(state.isAnimationsOn() && isActive);
289 metaMmLabel.setText(selected.mm);
290 metaFeeLabel.setText(selected.feeText());
291 metaContractLabel.setText(Format.money(selected.contract));
292
293 // Sections
294 boolean isLmsr = selected.type == MechanismType.LMSR;
295 lmsrSection.setVisible(isLmsr);
296 lmsrSection.setManaged(isLmsr);
297 orderBookSection.setVisible(!isLmsr);
298 orderBookSection.setManaged(!isLmsr);
299
300 if (isLmsr) {
301 refreshLmsr(selected);
302 } else {
303 refreshOrderBook(selected);
304 }
305
306 // Trade history table
307 var tRows = new ArrayList<Node>(selected.trades.size());
308 for (var t : selected.trades) {
309 tRows.add(buildTradeHistoryRow(t));
310 }
311 tradeHistoryRowsContainer.getChildren().setAll(tRows);
312
313 // Participants table
314 var pRows = new ArrayList<Node>(selected.participants.size());
315 for (var p : selected.participants) {
316 pRows.add(buildParticipantRow(p));
317 }
318 participantsRowsContainer.getChildren().setAll(pRows);
319
320 // Resolved banner
321 boolean isClosed = selected.status == EventStatus.SETTLED;
322 resolvedBanner.setVisible(isClosed);
323 resolvedBanner.setManaged(isClosed);
324 if (isClosed) {
325 resolvedBannerText.setText(
326 "Winning option: "
327 + selected.resolvedOption
328 + "  "
329 + selected.yesShares
330 + " YES / "
331 + selected.noShares
332 + " NO shares bought  contract emptied to holders, "
333 + selected.feePercent
334 + "% fee moved to "
335 + selected.mm
336 + ".");
337 }
338 }
339
340 private void refreshLmsr(EventData e) {
341 lmsrYesPriceLabel.setText(Format.money(e.yesPrice));
342 lmsrYesProgressBar.setProgress(e.yesPrice.doubleValue());
343 lmsrYesSharesLabel.setText(
344 e.yesShares + " shares  p = " + Format.prob(e.yesPrice.doubleValue()));
345
346 lmsrNoPriceLabel.setText(Format.money(e.noPrice));
347 lmsrNoProgressBar.setProgress(e.noPrice.doubleValue());
348 lmsrNoSharesLabel.setText(e.noShares + " shares  p = " + Format.prob(e.noPrice.doubleValue()));
349
350 lmsrChartSubLabel.setText("YES / NO  LMSR b = " + e.liquidityB);
351 var noPts = new ArrayList<ChartPoint>();
352 for (var p : e.chart) noPts.add(new ChartPoint(p.x(), 1.0 - p.y()));
353 Charts.setDualSeries(lmsrChart, "YES", e.chart, "NO", noPts);
354 }
355
356 private void refreshOrderBook(EventData e) {
357 obChartSubLabel.setText(
358 "last trade YES  base d = " + (e.baseValueD != null ? Format.money(e.baseValueD) : "—"));
359 Charts.setSingleSeries(obChart, "value", e.chart);
360
361 populateBookSide(
362 e.yesBook,
363 yesBookCountLabel,
364 yesLastLabel,
365 yesBestBidLabel,
366 yesBestAskLabel,
367 yesMidLabel,
368 yesSpreadLabel,
369 yesBookLadderList);
370 populateBookSide(
371 e.noBook,
372 noBookCountLabel,
373 noLastLabel,
374 noBestBidLabel,
375 noBestAskLabel,
376 noMidLabel,
377 noSpreadLabel,
378 noBookLadderList);
379 }
380
381 private void populateBookSide(
382 OrderBook book,
383 Label countLabel,
384 Label lastLabel,
385 Label bestBidLabel,
386 Label bestAskLabel,
387 Label midLabel,
388 Label spreadLabel,
389 VBox ladderList) {
390 if (book == null) {
391 ladderList.getChildren().clear();
392 countLabel.setText("0 orders");
393 for (var l : new Label[] {lastLabel, bestBidLabel, bestAskLabel, midLabel, spreadLabel}) {
394 l.setText("—");
395 }
396 return;
397 }
398
399 countLabel.setText(book.rows.size() + " orders");
400 lastLabel.setText(book.last != null ? book.last : "—");
401 bestBidLabel.setText(book.bid != null ? book.bid : "—");
402 bestAskLabel.setText(book.ask != null ? book.ask : "—");
403 midLabel.setText(book.mid != null ? book.mid : "—");
404 spreadLabel.setText(book.spread != null ? book.spread : "—");
405
406 var rows = new ArrayList<Node>(book.rows.size());
407 for (var r : book.rows) {
408 rows.add(new LadderItemView(r));
409 }
410 ladderList.getChildren().setAll(rows);
411 }
412
413 private Node buildTradeHistoryRow(TradeRow t) {
414 return new TradeHistoryItemView(t);
415 }
416
417 private Node buildParticipantRow(ParticipantRow p) {
418 return new ParticipantItemView(p);
419 }
420
421 // ---- FXML Handlers ----------------------------------------------------
422
423 @FXML
424 private void handleMethodAll() {
425 if (state != null) state.setFilterMethod(MechanismFilter.ALL);
426 }
427
428 @FXML
429 private void handleMethodLmsr() {
430 if (state != null) state.setFilterMethod(MechanismFilter.LMSR);
431 }
432
433 @FXML
434 private void handleMethodOb() {
435 if (state != null) state.setFilterMethod(MechanismFilter.ORDER_BOOK);
436 }
437
438 @FXML
439 private void handleStatusAll() {
440 if (state != null) state.setFilterStatus(null);
441 }
442
443 @FXML
444 private void handleStatusNotStarted() {
445 if (state != null) state.setFilterStatus(EventStatus.DRAFT);
446 }
447
448 @FXML
449 private void handleStatusActive() {
450 if (state != null) state.setFilterStatus(EventStatus.ACTIVE);
451 }
452
453 @FXML
454 private void handleStatusClosed() {
455 if (state != null) state.setFilterStatus(EventStatus.SETTLED);
456 }
457
458 @FXML
459 private void handleFeeAll() {
460 if (state != null) state.setFilterFee(CommissionFilter.ALL);
461 }
462
463 @FXML
464 private void handleFeePurchase() {
465 if (state != null) state.setFilterFee(CommissionFilter.PURCHASE);
466 }
467
468 @FXML
469 private void handleFeeClose() {
470 if (state != null) state.setFilterFee(CommissionFilter.CLOSE);
471 }
472
473 @FXML
474 private void handleNewEvent() {
475 if (onNewEvent != null) {
476 onNewEvent.run();
477 }
478 }
479
480 @FXML
481 private void handleOpenEvent() {
482 if (state != null) {
483 var e = state.selectedEvent();
484 if (e != null && onOpenEvent != null) {
485 onOpenEvent.accept(e);
486 }
487 }
488 }
489
490 @FXML
491 private void handleCloseEvent() {
492 if (onCloseEvent != null) {
493 onCloseEvent.run();
494 }
495 }
496}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/LoadDialogController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/LoadDialogController.java
new file mode 100644
index 0000000..e23cd9e
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/LoadDialogController.java
@@ -0,0 +1,70 @@
1package market.guess.ui.desktop.controllers;
2
3import javafx.animation.KeyFrame;
4import javafx.animation.KeyValue;
5import javafx.animation.Timeline;
6import javafx.fxml.FXML;
7import javafx.scene.control.Label;
8import javafx.scene.control.ProgressBar;
9import javafx.util.Duration;
10import market.guess.ui.desktop.task.InitialLoadTask;
11
12public final class LoadDialogController {
13 private Runnable onCancel;
14 private Timeline currentFillAnimation;
15
16 @FXML private Label pathLabel;
17 @FXML private ProgressBar progressBar;
18
19 public void setOnCancel(Runnable onCancel) {
20 this.onCancel = onCancel;
21 }
22
23 public void reset() {
24 if (currentFillAnimation != null) {
25 currentFillAnimation.stop();
26 currentFillAnimation = null;
27 }
28 if (progressBar != null) {
29 progressBar.setProgress(0.0);
30 }
31 }
32
33 public void show(InitialLoadTask task, String pendingFile) {
34 reset();
35 if (pathLabel != null) {
36 pathLabel.setText(pendingFile);
37 }
38 task.progressProperty()
39 .addListener(
40 (observable, oldValue, newValue) -> {
41 if (newValue == null) return;
42 double val = newValue.doubleValue();
43 if (val < 0) {
44 if (progressBar != null) {
45 progressBar.setProgress(0.0);
46 }
47 return;
48 }
49 if (progressBar != null) {
50 if (currentFillAnimation != null) {
51 currentFillAnimation.stop();
52 }
53 currentFillAnimation =
54 new Timeline(
55 new KeyFrame(
56 Duration.millis(100),
57 new KeyValue(progressBar.progressProperty(), val)));
58 currentFillAnimation.play();
59 }
60 });
61 }
62
63 @FXML
64 private void handleCancel() {
65 reset();
66 if (onCancel != null) {
67 onCancel.run();
68 }
69 }
70}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/ResolveDialogController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/ResolveDialogController.java
new file mode 100644
index 0000000..4183f7f
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/ResolveDialogController.java
@@ -0,0 +1,68 @@
1package market.guess.ui.desktop.controllers;
2
3import java.util.function.Consumer;
4import javafx.fxml.FXML;
5import javafx.scene.control.Button;
6import market.guess.ui.desktop.util.Views;
7
8public class ResolveDialogController {
9 private Runnable onCancel;
10 private Consumer<String> onConfirm;
11 private String selectedOption = null;
12
13 @FXML private Button resolveYesBtn;
14 @FXML private Button resolveNoBtn;
15 @FXML private Button confirmBtn;
16
17 @FXML
18 private void initialize() {
19 updateSelection();
20 }
21
22 public void setOnCancel(Runnable onCancel) {
23 this.onCancel = onCancel;
24 }
25
26 public void setOnConfirm(Consumer<String> onConfirm) {
27 this.onConfirm = onConfirm;
28 }
29
30 public void reset() {
31 selectedOption = null;
32 updateSelection();
33 }
34
35 private void updateSelection() {
36 Views.setSelected(resolveYesBtn, "YES".equals(selectedOption));
37 Views.setSelected(resolveNoBtn, "NO".equals(selectedOption));
38 boolean hasChoice = selectedOption != null;
39 confirmBtn.setDisable(!hasChoice);
40 confirmBtn.setText("Resolve as " + (hasChoice ? selectedOption : "…"));
41 }
42
43 @FXML
44 private void handleSelectYes() {
45 selectedOption = "YES";
46 updateSelection();
47 }
48
49 @FXML
50 private void handleSelectNo() {
51 selectedOption = "NO";
52 updateSelection();
53 }
54
55 @FXML
56 private void handleCancel() {
57 if (onCancel != null) {
58 onCancel.run();
59 }
60 }
61
62 @FXML
63 private void handleConfirm() {
64 if (onConfirm != null && selectedOption != null) {
65 onConfirm.accept(selectedOption);
66 }
67 }
68}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/UsersTabController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/UsersTabController.java
new file mode 100644
index 0000000..6211337
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/UsersTabController.java
@@ -0,0 +1,476 @@
1package market.guess.ui.desktop.controllers;
2
3import java.math.BigDecimal;
4import java.util.ArrayList;
5import java.util.function.Consumer;
6import javafx.collections.ListChangeListener;
7import javafx.css.PseudoClass;
8import javafx.fxml.FXML;
9import javafx.scene.Node;
10import javafx.scene.chart.LineChart;
11import javafx.scene.chart.NumberAxis;
12import javafx.scene.control.Button;
13import javafx.scene.control.Label;
14import javafx.scene.control.ListView;
15import javafx.scene.control.TextField;
16import javafx.scene.layout.HBox;
17import javafx.scene.layout.StackPane;
18import javafx.scene.layout.VBox;
19import market.guess.model.event.EventStatus;
20import market.guess.model.event.MechanismType;
21import market.guess.ui.desktop.AppState;
22import market.guess.ui.desktop.components.TradeSide;
23import market.guess.ui.desktop.components.UserEventItemView;
24import market.guess.ui.desktop.components.UserListCell;
25import market.guess.ui.desktop.components.graphic.Graphic;
26import market.guess.ui.desktop.components.graphic.SparklineGraphic;
27import market.guess.ui.desktop.model.UserData;
28import market.guess.ui.desktop.model.UserEventRow;
29import market.guess.ui.desktop.util.Charts;
30import market.guess.ui.desktop.util.Format;
31import market.guess.ui.desktop.util.Views;
32
33public class UsersTabController {
34 private static final PseudoClass BLOCKED = PseudoClass.getPseudoClass("blocked");
35 private static final PseudoClass MM = PseudoClass.getPseudoClass("mm");
36 private static final PseudoClass TRADER = PseudoClass.getPseudoClass("trader");
37 private static final PseudoClass NEGATIVE = PseudoClass.getPseudoClass("negative");
38 private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
39
40 @FunctionalInterface
41 public interface OrderPlacer {
42 void placeOrder(
43 String eventKey, String optionKey, String side, BigDecimal price, long quantity);
44 }
45
46 private AppState state;
47 private Runnable onCreateNewEvent;
48 private Consumer<String> onToast;
49 private OrderPlacer onPlaceOrder;
50 private Graphic sparkline;
51 private boolean updatingSelection = false;
52
53 @FXML private StackPane sparklineBox;
54 @FXML private ListView<UserData> userListContainer;
55 @FXML private VBox userDetailCard;
56 @FXML private Label userNameLabel;
57 @FXML private Label userRolePill;
58 @FXML private Label userSubLabel;
59 @FXML private Label userBalanceLabel;
60
61 @FXML private HBox blockedBanner;
62 @FXML private LineChart<Number, Number> balanceChart;
63 @FXML private VBox participationRowsContainer;
64
65 @FXML private VBox tradePanel;
66 @FXML private Label noTradeEventLabel;
67 @FXML private VBox tradeContentBox;
68 @FXML private Label tradeEventTitle;
69 @FXML private Label tradeEventSub;
70 @FXML private VBox tradeYesOptCard;
71 @FXML private Label tradeYesPriceLabel;
72 @FXML private Label tradeYesHeldLabel;
73 @FXML private VBox tradeNoOptCard;
74 @FXML private Label tradeNoPriceLabel;
75 @FXML private Label tradeNoHeldLabel;
76 @FXML private Button tradeBuyBtn;
77 @FXML private Button tradeSellBtn;
78 @FXML private TextField tradeQtyField;
79 @FXML private TextField tradePriceField;
80 @FXML private Label tradeCostLabel;
81 @FXML private Button tradeSubmitBtn;
82 @FXML private Label tradeHintLabel;
83
84 public void init(AppState state) {
85 this.state = state;
86 sparkline = SparklineGraphic.create();
87 sparklineBox.getChildren().setAll(sparkline.node);
88
89 userListContainer.setCellFactory(lv -> new UserListCell());
90 userListContainer
91 .getSelectionModel()
92 .selectedItemProperty()
93 .addListener(
94 (obs, oldVal, newVal) -> {
95 if (updatingSelection) return;
96 if (newVal != null && state != null) {
97 state.setActingUser(newVal);
98 }
99 });
100
101 state
102 .actingUserProperty()
103 .addListener(
104 (obs, oldVal, newVal) -> {
105 if (newVal != null) {
106 if (!newVal.equals(userListContainer.getSelectionModel().getSelectedItem())) {
107 updatingSelection = true;
108 try {
109 userListContainer.getSelectionModel().select(newVal);
110 } finally {
111 updatingSelection = false;
112 }
113 }
114 }
115 refreshUserDetail(newVal);
116 });
117
118 state
119 .selectedEventProperty()
120 .addListener(
121 (obs, oldVal, newVal) -> {
122 refreshTradePanel(state.actingUser());
123 });
124
125 state
126 .tradeOptionYesProperty()
127 .addListener((obs, oldVal, newVal) -> refreshTradePanel(state.actingUser()));
128
129 state
130 .tradeSideProperty()
131 .addListener((obs, oldVal, newVal) -> refreshTradePanel(state.actingUser()));
132
133 state
134 .getUsers()
135 .addListener(
136 (ListChangeListener<UserData>)
137 c -> {
138 refreshUserList();
139 refreshUserDetail(state.actingUser());
140 });
141 state.animationsOnProperty().addListener((obs, oldVal, newVal) -> sparkline.setPlaying(newVal));
142
143 tradeQtyField
144 .textProperty()
145 .addListener(
146 (obs, o, n) -> {
147 if (state != null) state.setTradeQty(n);
148 validateTradeInputs();
149 });
150
151 tradePriceField
152 .textProperty()
153 .addListener(
154 (obs, o, n) -> {
155 if (state != null) state.setTradePrice(n);
156 validateTradeInputs();
157 });
158 }
159
160 public void setOnCreateNewEvent(Runnable onCreateNewEvent) {
161 this.onCreateNewEvent = onCreateNewEvent;
162 }
163
164 public void setOnToast(Consumer<String> onToast) {
165 this.onToast = onToast;
166 }
167
168 public void setOnPlaceOrder(OrderPlacer onPlaceOrder) {
169 this.onPlaceOrder = onPlaceOrder;
170 }
171
172 public void refresh() {
173 if (state == null) return;
174 refreshUserList();
175 refreshUserDetail(state.actingUser());
176 }
177
178 private void refreshUserList() {
179 if (state == null) return;
180 updatingSelection = true;
181 try {
182 userListContainer.getItems().setAll(state.getUsers());
183 var actor = state.actingUser();
184 UserData match = null;
185 if (actor != null) {
186 for (var u : state.getUsers()) {
187 if (u.name.equals(actor.name)) {
188 match = u;
189 break;
190 }
191 }
192 }
193 if (match != null) {
194 userListContainer.getSelectionModel().select(match);
195 } else if (!state.getUsers().isEmpty()) {
196 userListContainer.getSelectionModel().select(0);
197 }
198 } finally {
199 updatingSelection = false;
200 }
201 }
202
203 private void refreshUserDetail(UserData actor) {
204 if (actor == null) {
205 userDetailCard.setVisible(false);
206 return;
207 }
208 userDetailCard.setVisible(true);
209
210 userNameLabel.setText(actor.name);
211 userRolePill.setText(actor.blocked ? "BLOCKED" : actor.isMm ? "MARKET MAKER" : "TRADER");
212 userRolePill.pseudoClassStateChanged(BLOCKED, actor.blocked);
213 userRolePill.pseudoClassStateChanged(MM, !actor.blocked && actor.isMm);
214 userRolePill.pseudoClassStateChanged(TRADER, !actor.blocked && !actor.isMm);
215
216 userSubLabel.setText("Active in " + actor.events.size() + " events");
217 userBalanceLabel.setText(Format.money(actor.balance));
218 userBalanceLabel.pseudoClassStateChanged(NEGATIVE, actor.balance.signum() < 0);
219 sparkline.node.pseudoClassStateChanged(NEGATIVE, actor.balance.signum() < 0);
220 sparkline.setPlaying(state.isAnimationsOn());
221
222 // Blocked Banner
223 blockedBanner.setVisible(actor.blocked);
224 blockedBanner.setManaged(actor.blocked);
225
226 // Balance Chart
227 Charts.setSingleSeries(balanceChart, "balance", actor.balanceHistory);
228 if (balanceChart.getYAxis() instanceof NumberAxis yAxis) {
229 Charts.scaleYAxis(yAxis, actor.balanceHistory, actor.balance.doubleValue());
230 }
231
232 // Participation Table
233 var evRows = new ArrayList<Node>(actor.events.size());
234 for (var ev : actor.events) {
235 evRows.add(buildParticipationRow(ev));
236 }
237 participationRowsContainer.getChildren().setAll(evRows);
238
239 // Trade Panel
240 refreshTradePanel(actor);
241 }
242
243 private void refreshTradePanel(UserData actor) {
244 if (state == null) return;
245 var e = state.selectedEvent();
246
247 if (e == null) {
248 noTradeEventLabel.setVisible(true);
249 noTradeEventLabel.setManaged(true);
250 tradeContentBox.setVisible(false);
251 tradeContentBox.setManaged(false);
252 return;
253 }
254
255 noTradeEventLabel.setVisible(false);
256 noTradeEventLabel.setManaged(false);
257 tradeContentBox.setVisible(true);
258 tradeContentBox.setManaged(true);
259
260 tradeEventTitle.setText(e.num + ". " + e.name);
261 tradeEventSub.setText(
262 (e.type == MechanismType.LMSR ? "LMSR" : "Order Book")
263 + "  Status: "
264 + e.status
265 + "  Fee: "
266 + e.feeText());
267
268 int yesHeld = 0, noHeld = 0;
269 if (actor != null && actor.events != null) {
270 for (var ev : actor.events) {
271 if (ev.eventId().equals(e.id)) {
272 yesHeld = ev.yes();
273 noHeld = ev.no();
274 break;
275 }
276 }
277 }
278
279 tradeYesPriceLabel.setText(Format.money(e.yesPrice));
280 tradeYesHeldLabel.setText("You hold " + yesHeld + " shares");
281
282 tradeNoPriceLabel.setText(Format.money(e.noPrice));
283 tradeNoHeldLabel.setText("You hold " + noHeld + " shares");
284
285 Views.setSelected(tradeYesOptCard, state.isTradeOptionYes());
286 Views.setSelected(tradeNoOptCard, !state.isTradeOptionYes());
287
288 Views.setSelected(tradeBuyBtn, state.getTradeSide() == TradeSide.BUY);
289 Views.setSelected(tradeSellBtn, state.getTradeSide() == TradeSide.SELL);
290
291 if (tradeQtyField.getText().isEmpty()) tradeQtyField.setText(state.getTradeQty());
292 if (tradePriceField.getText().isEmpty()) tradePriceField.setText(state.getTradePrice());
293
294 tradePriceField.setVisible(e.type == MechanismType.ORDER_BOOK);
295 tradePriceField.setManaged(e.type == MechanismType.ORDER_BOOK);
296
297 tradeSubmitBtn.setText(
298 state.getTradeSide() == TradeSide.BUY ? "Place Buy Order" : "Place Sell Order");
299
300 validateTradeInputs();
301 }
302
303 private boolean validateTradeInputs() {
304 if (state == null) return false;
305 var e = state.selectedEvent();
306 var actor = state.actingUser();
307 if (e == null || actor == null) return false;
308
309 boolean valid = true;
310 String qtyText = tradeQtyField.getText() != null ? tradeQtyField.getText().trim() : "";
311 long qty = 0;
312 if (qtyText.isEmpty()) {
313 valid = false;
314 tradeQtyField.pseudoClassStateChanged(ERROR, true);
315 tradeHintLabel.setText("Please enter the number of shares.");
316 } else {
317 try {
318 qty = Long.parseLong(qtyText);
319 if (qty <= 0) {
320 valid = false;
321 tradeQtyField.pseudoClassStateChanged(ERROR, true);
322 tradeHintLabel.setText("Shares must be a positive number.");
323 } else {
324 tradeQtyField.pseudoClassStateChanged(ERROR, false);
325 }
326 } catch (NumberFormatException nfe) {
327 valid = false;
328 tradeQtyField.pseudoClassStateChanged(ERROR, true);
329 tradeHintLabel.setText("Shares must be a valid integer.");
330 }
331 }
332
333 BigDecimal price = null;
334 if (e.type == MechanismType.ORDER_BOOK) {
335 String priceText = tradePriceField.getText() != null ? tradePriceField.getText().trim() : "";
336 if (priceText.isEmpty()) {
337 valid = false;
338 tradePriceField.pseudoClassStateChanged(ERROR, true);
339 if (valid) {
340 tradeHintLabel.setText("Please enter a price per share.");
341 }
342 } else {
343 try {
344 price = new BigDecimal(priceText);
345 if (price.compareTo(BigDecimal.ZERO) <= 0) {
346 valid = false;
347 tradePriceField.pseudoClassStateChanged(ERROR, true);
348 tradeHintLabel.setText("Price per share must be positive.");
349 } else {
350 tradePriceField.pseudoClassStateChanged(ERROR, false);
351 }
352 } catch (Exception nfe) {
353 valid = false;
354 tradePriceField.pseudoClassStateChanged(ERROR, true);
355 tradeHintLabel.setText("Price must be a valid decimal number.");
356 }
357 }
358 } else {
359 tradePriceField.pseudoClassStateChanged(ERROR, false);
360 price = state.isTradeOptionYes() ? e.yesPrice : e.noPrice;
361 }
362
363 if (valid && qty > 0 && price != null) {
364 BigDecimal cost = price.multiply(BigDecimal.valueOf(qty));
365 tradeCostLabel.setText(Format.money(cost));
366 if (actor.blocked) {
367 tradeHintLabel.setText("Trading is disabled because this account is blocked.");
368 tradeSubmitBtn.setDisable(true);
369 } else if (e.status != EventStatus.ACTIVE) {
370 tradeHintLabel.setText("Trading is only available for ACTIVE events.");
371 tradeSubmitBtn.setDisable(true);
372 } else if (state.getTradeSide() == TradeSide.SELL) {
373 int held = 0;
374 if (actor.events != null) {
375 for (var evRow : actor.events) {
376 if (evRow.eventId().equals(e.id)) {
377 held = state.isTradeOptionYes() ? evRow.yes() : evRow.no();
378 break;
379 }
380 }
381 }
382 if (!actor.isMm && qty > held) {
383 tradeHintLabel.setText("Cannot sell " + qty + " shares; you only hold " + held + ".");
384 tradeSubmitBtn.setDisable(true);
385 } else {
386 tradeHintLabel.setText("Sell order will match existing bids or rest as an ask.");
387 tradeSubmitBtn.setDisable(false);
388 }
389 } else {
390 if (actor.balance.compareTo(cost) < 0) {
391 tradeHintLabel.setText(
392 "Insufficient balance. Required: "
393 + Format.money(cost)
394 + ", available: "
395 + Format.money(actor.balance));
396 tradeSubmitBtn.setDisable(true);
397 } else {
398 tradeHintLabel.setText(
399 e.type == MechanismType.ORDER_BOOK
400 ? "Buy order will match existing asks or rest as a bid."
401 : "Orders execute immediately against the market maker.");
402 tradeSubmitBtn.setDisable(false);
403 }
404 }
405 } else {
406 tradeCostLabel.setText("—");
407 tradeSubmitBtn.setDisable(true);
408 }
409
410 return valid;
411 }
412
413 private Node buildParticipationRow(UserEventRow ev) {
414 var row = new UserEventItemView(ev);
415 row.setOnMouseClicked(
416 e -> {
417 if (state != null) {
418 state.setSelectedEventId(ev.eventId());
419 }
420 });
421 return row;
422 }
423
424 // ---- FXML Handlers ----------------------------------------------------
425
426 @FXML
427 private void handleCreateNewEvent() {
428 if (onCreateNewEvent != null) {
429 onCreateNewEvent.run();
430 }
431 }
432
433 @FXML
434 private void handleSelectTradeYes() {
435 if (state != null) state.setTradeOptionYes(true);
436 }
437
438 @FXML
439 private void handleSelectTradeNo() {
440 if (state != null) state.setTradeOptionYes(false);
441 }
442
443 @FXML
444 private void handleTradeBuy() {
445 if (state != null) state.setTradeSide(TradeSide.BUY);
446 }
447
448 @FXML
449 private void handleTradeSell() {
450 if (state != null) state.setTradeSide(TradeSide.SELL);
451 }
452
453 @FXML
454 private void handleTradeSubmit() {
455 if (state == null) return;
456 var e = state.selectedEvent();
457 var u = state.actingUser();
458 if (e == null || u == null) return;
459 if (!validateTradeInputs()) return;
460
461 long qty = Long.parseLong(tradeQtyField.getText().trim());
462 BigDecimal price =
463 (e.type == MechanismType.ORDER_BOOK)
464 ? new BigDecimal(tradePriceField.getText().trim())
465 : (state.isTradeOptionYes() ? e.yesPrice : e.noPrice);
466
467 String optionKey = state.isTradeOptionYes() ? "YES" : "NO";
468 String side = state.getTradeSide() == TradeSide.SELL ? "SELL" : "BUY";
469
470 if (onPlaceOrder != null) {
471 onPlaceOrder.placeOrder(e.id, optionKey, side, price, qty);
472 } else if (onToast != null) {
473 onToast.accept("Order placed: " + side + " " + qty + " " + optionKey + " on #" + e.num);
474 }
475 }
476}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/BookRow.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/BookRow.java
new file mode 100644
index 0000000..8162419
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/BookRow.java
@@ -0,0 +1,3 @@
1package market.guess.ui.desktop.model;
2
3public record BookRow(String side, String user, int qty, String price, boolean isBid) {}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/ChartPoint.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/ChartPoint.java
new file mode 100644
index 0000000..9dd845c
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/ChartPoint.java
@@ -0,0 +1,3 @@
1package market.guess.ui.desktop.model;
2
3public record ChartPoint(double x, double y) {}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/EventData.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/EventData.java
new file mode 100644
index 0000000..f3d0bc4
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/EventData.java
@@ -0,0 +1,81 @@
1package market.guess.ui.desktop.model;
2
3import java.math.BigDecimal;
4import java.util.ArrayList;
5import java.util.List;
6import market.guess.model.event.CommissionTiming;
7import market.guess.model.event.EventStatus;
8import market.guess.model.event.MechanismType;
9
10public final class EventData {
11 public final String id;
12
13 public final int num;
14
15 public final String name;
16 public final String desc;
17 public final MechanismType type;
18 public EventStatus status;
19 public final String mm;
20 public final int feePercent;
21 public final CommissionTiming feeMode;
22 public BigDecimal contract;
23 public final Integer liquidityB; // LMSR only
24 public final BigDecimal baseValueD; // Order Book only
25 public BigDecimal yesPrice = new BigDecimal(0.5);
26 public BigDecimal noPrice = new BigDecimal(0.5);
27 public int yesShares, noShares;
28 public final List<TradeRow> trades = new ArrayList<>();
29 public final List<ChartPoint> chart = new ArrayList<>();
30 public OrderBook yesBook, noBook;
31 public final List<ParticipantRow> participants = new ArrayList<>();
32 public String resolvedOption; // null unless CLOSED
33
34 public EventData(
35 final String id,
36 final int num,
37 final String name,
38 final String desc,
39 final MechanismType type,
40 final EventStatus status,
41 final String mm,
42 final int feePercent,
43 final CommissionTiming feeMode,
44 final double contract,
45 final Integer liquidityB,
46 final BigDecimal baseValueD) {
47 this.id = id;
48 this.num = num;
49 this.name = name;
50 this.desc = desc;
51 this.type = type;
52 this.status = status;
53 this.mm = mm;
54 this.feePercent = feePercent;
55 this.feeMode = feeMode;
56 this.contract = BigDecimal.valueOf(contract);
57 this.liquidityB = liquidityB;
58 this.baseValueD = baseValueD;
59 }
60
61 public String getId() {
62 return id;
63 }
64
65 public String feeText() {
66 return feePercent + "% on " + (feeMode == CommissionTiming.ON_PURCHASE ? "purchase" : "close");
67 }
68
69 @Override
70 public boolean equals(Object o) {
71 if (this == o) return true;
72 if (o == null || getClass() != o.getClass()) return false;
73 EventData eventData = (EventData) o;
74 return id != null && id.equals(eventData.id);
75 }
76
77 @Override
78 public int hashCode() {
79 return id != null ? id.hashCode() : 0;
80 }
81}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/OrderBook.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/OrderBook.java
new file mode 100644
index 0000000..230798d
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/OrderBook.java
@@ -0,0 +1,16 @@
1package market.guess.ui.desktop.model;
2
3import java.util.ArrayList;
4import java.util.List;
5
6public final class OrderBook {
7 public final String optionName;
8 public final boolean yesOption;
9 public String last, bid, ask, mid, spread;
10 public final List<BookRow> rows = new ArrayList<>();
11
12 public OrderBook(String optionName, boolean yesOption) {
13 this.optionName = optionName;
14 this.yesOption = yesOption;
15 }
16}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/ParticipantRow.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/ParticipantRow.java
new file mode 100644
index 0000000..256a974
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/ParticipantRow.java
@@ -0,0 +1,4 @@
1package market.guess.ui.desktop.model;
2
3public record ParticipantRow(
4 String user, String tag, int yes, int no, String value, String fees, String pnl) {}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/TradeRow.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/TradeRow.java
new file mode 100644
index 0000000..d95b2d6
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/TradeRow.java
@@ -0,0 +1,4 @@
1package market.guess.ui.desktop.model;
2
3public record TradeRow(
4 int n, String user, String option, boolean yesOption, int shares, String paid) {}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserData.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserData.java
new file mode 100644
index 0000000..62ad526
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserData.java
@@ -0,0 +1,41 @@
1package market.guess.ui.desktop.model;
2
3import java.math.BigDecimal;
4import java.util.ArrayList;
5import java.util.List;
6
7public final class UserData {
8 public final String name;
9
10 public String getName() {
11 return name;
12 }
13
14 public String role;
15 public BigDecimal balance;
16 public final boolean blocked;
17 public final boolean isMm;
18 public final List<ChartPoint> balanceHistory = new ArrayList<>();
19 public final List<UserEventRow> events = new ArrayList<>();
20
21 public UserData(String name, String role, double balance, boolean blocked, boolean isMm) {
22 this.name = name;
23 this.role = role;
24 this.balance = BigDecimal.valueOf(balance);
25 this.blocked = blocked;
26 this.isMm = isMm;
27 }
28
29 @Override
30 public boolean equals(Object o) {
31 if (this == o) return true;
32 if (o == null || getClass() != o.getClass()) return false;
33 UserData userData = (UserData) o;
34 return name != null && name.equalsIgnoreCase(userData.name);
35 }
36
37 @Override
38 public int hashCode() {
39 return name != null ? name.toLowerCase().hashCode() : 0;
40 }
41}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserEventRow.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserEventRow.java
new file mode 100644
index 0000000..7c15530
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserEventRow.java
@@ -0,0 +1,4 @@
1package market.guess.ui.desktop.model;
2
3public record UserEventRow(
4 String eventId, String eventName, String role, String type, int yes, int no, String pl) {}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/task/InitialLoadTask.java b/ui-desktop/src/main/java/market/guess/ui/desktop/task/InitialLoadTask.java
new file mode 100644
index 0000000..e49b0c4
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/task/InitialLoadTask.java
@@ -0,0 +1,57 @@
1package market.guess.ui.desktop.task;
2
3import java.nio.file.Path;
4import javafx.concurrent.Task;
5import market.guess.api.CatalogContext;
6import market.guess.api.LoadResult;
7
8public final class InitialLoadTask extends Task<LoadResult> {
9 private final CatalogContext catalog;
10 private final Path path;
11
12 public InitialLoadTask(CatalogContext catalog, Path path) {
13 this.catalog = catalog;
14 this.path = path;
15 }
16
17 @Override
18 protected LoadResult call() throws Exception {
19 updateProgress(0, 100);
20 if (isCancelled()) {
21 return null;
22 }
23
24 for (int i = 0; i < 20; i++) {
25 if (isCancelled()) {
26 return null;
27 }
28 try {
29 Thread.sleep(100);
30 } catch (InterruptedException e) {
31 if (isCancelled()) {
32 return null;
33 }
34 Thread.currentThread().interrupt();
35 break;
36 }
37 updateProgress((i + 1) * 5, 100);
38 }
39
40 if (isCancelled()) {
41 return null;
42 }
43
44 var result = catalog.loadEvents(path);
45
46 if (isCancelled()) {
47 return null;
48 }
49
50 if (!result.isSuccess()) {
51 throw new RuntimeException(result.getDetails());
52 }
53
54 updateProgress(100, 100);
55 return result.getData();
56 }
57}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/util/Charts.java b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Charts.java
new file mode 100644
index 0000000..54f67ce
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Charts.java
@@ -0,0 +1,202 @@
1package market.guess.ui.desktop.util;
2
3import java.time.Instant;
4import java.time.LocalDate;
5import java.time.ZoneId;
6import java.time.format.DateTimeFormatter;
7import java.util.List;
8import javafx.scene.chart.LineChart;
9import javafx.scene.chart.NumberAxis;
10import javafx.scene.chart.XYChart;
11import javafx.util.StringConverter;
12import market.guess.ui.desktop.model.ChartPoint;
13
14/** Utilities for populating LineChart series and formatting time/value axes. */
15public final class Charts {
16 private Charts() {}
17
18 public static final long BASE_TIME =
19 LocalDate.of(2026, 9, 12).atTime(9, 0).atZone(ZoneId.systemDefault()).toEpochSecond();
20
21 public static double toEpochSecond(double x) {
22 if (x >= 1_000_000_000) {
23 return x;
24 }
25 return BASE_TIME + (long) (x * 1800);
26 }
27
28 public static void setSingleSeries(
29 LineChart<Number, Number> chart, String name, List<ChartPoint> pts) {
30 configureTimeXAxis(chart, pts);
31 chart.getData().setAll(List.of(series(name, pts)));
32 }
33
34 public static void setDualSeries(
35 LineChart<Number, Number> chart,
36 String name1,
37 List<ChartPoint> pts1,
38 String name2,
39 List<ChartPoint> pts2) {
40 configureTimeXAxis(chart, (pts1 != null && !pts1.isEmpty()) ? pts1 : pts2);
41 chart.getData().setAll(List.of(series(name1, pts1), series(name2, pts2)));
42 }
43
44 public static void configureTimeXAxis(LineChart<Number, Number> chart, List<ChartPoint> pts) {
45 if (!(chart.getXAxis() instanceof NumberAxis xAxis)) return;
46
47 xAxis.setTickMarkVisible(true);
48 xAxis.setTickLabelsVisible(true);
49 xAxis.setMinorTickVisible(false);
50 xAxis.setForceZeroInRange(false);
51
52 if (pts == null || pts.isEmpty()) {
53 xAxis.setAutoRanging(true);
54 return;
55 }
56
57 double min = pts.stream().mapToDouble(p -> toEpochSecond(p.x())).min().orElse(BASE_TIME);
58 double max = pts.stream().mapToDouble(p -> toEpochSecond(p.x())).max().orElse(BASE_TIME + 3600);
59
60 if (min == max) {
61 min -= 60;
62 max += 60;
63 }
64
65 double range = max - min;
66 double targetInterval = range / 5.5;
67
68 double[] cleanIntervals = {
69 1, 5, 10, 15, 30, 60, 300, 600, 900, 1800, 3600, 7200, 14400, 21600, 43200, 86400
70 };
71 double tickUnit = cleanIntervals[cleanIntervals.length - 1];
72 for (double unit : cleanIntervals) {
73 if (unit >= targetInterval) {
74 tickUnit = unit;
75 break;
76 }
77 }
78
79 double lower = Math.floor(min / tickUnit) * tickUnit;
80 double upper = Math.ceil(max / tickUnit) * tickUnit;
81 if (upper <= max) {
82 upper += tickUnit;
83 }
84
85 xAxis.setAutoRanging(false);
86 xAxis.setLowerBound(lower);
87 xAxis.setUpperBound(upper);
88 xAxis.setTickUnit(tickUnit);
89
90 DateTimeFormatter fmt =
91 (range > 86400)
92 ? DateTimeFormatter.ofPattern("MM-dd HH:mm").withZone(ZoneId.systemDefault())
93 : (range < 300)
94 ? DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault())
95 : DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault());
96
97 xAxis.setTickLabelFormatter(
98 new StringConverter<Number>() {
99 @Override
100 public String toString(Number n) {
101 if (n == null) return "";
102 try {
103 return fmt.format(Instant.ofEpochSecond(n.longValue()));
104 } catch (Exception e) {
105 return n.toString();
106 }
107 }
108
109 @Override
110 public Number fromString(String s) {
111 return 0;
112 }
113 });
114 }
115
116 /**
117 * Scales the Y axis dynamically with sensible headroom and round tick marks so values (like
118 * balances > $1,500) never get cut off at the top or bottom.
119 */
120 public static void scaleYAxis(NumberAxis yAxis, List<ChartPoint> pts, Double currentVal) {
121 if (yAxis == null) return;
122 if ((pts == null || pts.isEmpty()) && currentVal == null) {
123 yAxis.setAutoRanging(true);
124 return;
125 }
126
127 double min =
128 (pts != null && !pts.isEmpty())
129 ? pts.stream().mapToDouble(ChartPoint::y).min().orElse(0.0)
130 : (currentVal != null ? currentVal : 0.0);
131 double max =
132 (pts != null && !pts.isEmpty())
133 ? pts.stream().mapToDouble(ChartPoint::y).max().orElse(100.0)
134 : (currentVal != null ? currentVal : 100.0);
135
136 if (currentVal != null) {
137 min = Math.min(min, currentVal);
138 max = Math.max(max, currentVal);
139 }
140
141 if (max <= 0 && min <= 0) {
142 double absMax = Math.abs(min);
143 double unit = niceNum(absMax / 4.0, true);
144 if (unit <= 0) unit = 50;
145 double lower = Math.floor(min / unit) * unit - unit;
146 yAxis.setAutoRanging(false);
147 yAxis.setLowerBound(lower);
148 yAxis.setUpperBound(0.0);
149 yAxis.setTickUnit(unit);
150 return;
151 }
152
153 double targetMax = max > 0 ? max * 1.15 : 100.0;
154 double targetMin = min < 0 ? min * 1.20 : 0.0;
155 double range = targetMax - targetMin;
156 double unit = niceNum(range / 5.0, true);
157 if (unit <= 0) unit = 50;
158
159 double lower = targetMin < 0 ? Math.floor(targetMin / unit) * unit : 0.0;
160 double upper = Math.ceil(targetMax / unit) * unit;
161 if (upper <= max) {
162 upper += unit;
163 }
164
165 yAxis.setAutoRanging(false);
166 yAxis.setLowerBound(lower);
167 yAxis.setUpperBound(upper);
168 yAxis.setTickUnit(unit);
169 }
170
171 private static double niceNum(double range, boolean round) {
172 if (range <= 0) return 1.0;
173 double exponent = Math.floor(Math.log10(range));
174 double fraction = range / Math.pow(10, exponent);
175 double niceFraction;
176
177 if (round) {
178 if (fraction < 1.5) niceFraction = 1.0;
179 else if (fraction < 3.0) niceFraction = 2.0;
180 else if (fraction < 7.0) niceFraction = 5.0;
181 else niceFraction = 10.0;
182 } else {
183 if (fraction <= 1.0) niceFraction = 1.0;
184 else if (fraction <= 2.0) niceFraction = 2.0;
185 else if (fraction <= 5.0) niceFraction = 5.0;
186 else niceFraction = 10.0;
187 }
188
189 return niceFraction * Math.pow(10, exponent);
190 }
191
192 private static XYChart.Series<Number, Number> series(String name, List<ChartPoint> pts) {
193 var s = new XYChart.Series<Number, Number>();
194 s.setName(name);
195 if (pts != null) {
196 for (var p : pts) {
197 s.getData().add(new XYChart.Data<>(toEpochSecond(p.x()), p.y()));
198 }
199 }
200 return s;
201 }
202}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/util/Format.java b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Format.java
new file mode 100644
index 0000000..02380bc
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Format.java
@@ -0,0 +1,21 @@
1package market.guess.ui.desktop.util;
2
3import java.math.BigDecimal;
4
5/** Pure formatting helpers for monetary amounts and probabilities. */
6public final class Format {
7 private Format() {}
8
9 public static String money(BigDecimal n) {
10 if (n == null) return "$0.00";
11 return (n.signum() < 0 ? "-$" : "$") + String.format("%.2f", n.abs());
12 }
13
14 public static String money(double n) {
15 return money(BigDecimal.valueOf(n));
16 }
17
18 public static String prob(double n) {
19 return String.format("%.2f", n);
20 }
21}
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/util/Views.java b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Views.java
new file mode 100644
index 0000000..50d9c36
--- /dev/null
+++ b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Views.java
@@ -0,0 +1,41 @@
1package market.guess.ui.desktop.util;
2
3import java.io.IOException;
4import java.io.UncheckedIOException;
5import java.net.URL;
6import javafx.css.PseudoClass;
7import javafx.fxml.FXMLLoader;
8import javafx.scene.Node;
9import javafx.scene.Parent;
10import market.guess.ui.desktop.AppView;
11
12/** Shared JavaFX view mechanics, pseudo-class management, and FXML root loading. */
13public final class Views {
14 private Views() {}
15
16 public static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
17
18 public static void setSelected(Node node, boolean selected) {
19 if (node != null) {
20 node.pseudoClassStateChanged(SELECTED, selected);
21 }
22 }
23
24 public static void loadRoot(Parent root, String fxmlName) {
25 URL res = AppView.class.getResource(fxmlName);
26 if (res == null) {
27 res = AppView.class.getResource("/market/guess/ui/desktop/" + fxmlName);
28 }
29 if (res == null) {
30 throw new IllegalArgumentException("Cannot find FXML resource: " + fxmlName);
31 }
32 FXMLLoader loader = new FXMLLoader(res);
33 loader.setRoot(root);
34 loader.setController(root);
35 try {
36 loader.load();
37 } catch (IOException e) {
38 throw new UncheckedIOException("Failed to load " + fxmlName, e);
39 }
40 }
41}