Squashed 'third_party/allwpilib/' changes from 66b57f032..e473a00f9
e473a00f9 [wpiutil] Base64: Add unsigned span/vector variants (#3702)
52f2d580e [wpiutil] raw_uv_ostream: Add reset() (#3701)
d7b1e3576 [wpiutil] WebSocket: move std::function (#3700)
93799fbe9 [examples] Fix description of TrapezoidProfileSubsystem (#3699)
b84644740 [wpimath] Document pose estimator states, inputs, and outputs (#3698)
2dc35c139 [wpimath] Fix classpaths for JNI class loads (#3697)
2cb171f6f [docs] Set Doxygen extract_all to true and fix Doxygen failures (#3695)
a939cd9c8 [wpimath] Print uncontrollable/unobservable models in LQR and KF (#3694)
d5270d113 [wpimath] Clean up C++ StateSpaceUtil tests (#3692)
b20903960 [wpimath] Remove redundant discretization tests from StateSpaceUtilTest (#3689)
c0cb545b4 [wpilibc] Add deprecated Doxygen attribute to SpeedController (#3691)
35c9f66a7 [wpilib] Rename PneumaticsHub to PneumaticHub (#3686)
796d03d10 [wpiutil] Remove unused LLVM header (#3688)
8723caf78 [wpilibj] Make Java TrapezoidProfile.Constraints an immutable class (#3687)
187f50a34 [wpimath] Catch incorrect parameters to state-space models earlier (#3680)
8d04606c4 Replace instances of frc-characterization with SysId (NFC) (#3681)
b82d4f6e5 [hal, cscore, ntcore] Use WPI common handle type base
87e34967e [wpiutil] Add synchronization primitives
e32499c54 [wpiutil] Add ParallelTcpConnector (#3655)
aa0b49228 [wpilib] Remove redundant "quick turn" docs for curvature drive (NFC) (#3674)
57301a7f9 [hal] REVPH: Start closed-loop compressor control on init (#3673)
d1842ea8f [wpilib] Improve interrupt docs (NFC) (#3679)
558151061 [wpiutil] Add DsClient (#3654)
181723e57 Replace `.to<double>()` and `.template to<double>()` with `.value()` (#3667)
6bc1db44b [commands] Add pointer overload of AddRequirements (artf6003) (#3669)
737b57ed5 [wpimath] Update to drake v0.35.0 (#3665)
4d287d1ae [build] Upgrade WPIJREArtifact to JRE 2022-11.0.12u5 (#3666)
f26eb5ada [hal] Fix another typo (get -> gets) (NFC) (#3663)
94ed275ba [hal] Fix misspelling (numer -> number) (NFC) (#3662)
ac2f44da3 [wpiutil] uv: use move for std::function (#3653)
75fa1fbfb [wpiutil] json::serializer: Optimize construction (#3647)
5e689faea [wpiutil] Import MessagePack implementation (mpack) (#3650)
649a50b40 [wpiutil] Add LEB128 byte-by-byte reader (#3652)
e94397a97 [wpiutil] Move json_serializer.h to public headers (#3646)
4ec58724d [wpiutil] uv::Tcp: Clarify SetNoDelay documentation (#3649)
8cb294aa4 [wpiutil] WebSocket: Make Shutdown() public (#3651)
2b3a9a52b [wpiutil] json: Fix map iterator key() for std::string_view (#3645)
138cbb94b [wpiutil] uv::Async: Add direct call for no-parameter specialization (#3648)
e56d6dea8 [ci] Update testbench pool image to ubuntu-latest (#3643)
43f30e44e [build] Enable comments in doxygen source files (#3644)
9e6db17ef [build] Enable doxygen preprocessor expansion of WPI_DEPRECATED (#3642)
0e631ad2f Add WPILib version to issue template (#3641)
6229d8d2f [build] Docs: set case_sense_names to false (#3392)
4647d09b5 [docs] Fix Doxygen warnings, add CI docs lint job (#3639)
4ad3a5402 [hal] Fix PWM allocation channel (#3637)
05e5feac4 [docs] Fix brownout docs (NFC) (#3638)
67df469c5 [examples] Remove old command-based templates and examples (#3263)
689e9ccfb [hal, wpilib] Add brownout voltage configuration (#3632)
9cd4bc407 [docs] Add useLocal to avoid using installer artifacts (#3634)
61996c2bb [cscore] Fix Java direct callback notifications (#3631)
6d3dd99eb [build] Update to newest native-utils (#3633)
f0b484892 [wpiutil] Fix StringMap iterator equality check (#3629)
8352cbb7a Update development build instructions for 2022 (#3616)
6da08b71d [examples] Fix Intermediate Vision Java Example description (#3628)
5d99059bf [wpiutil] Remove optional.h (#3627)
fa41b106a [glass, wpiutil] Add missing format args (#3626)
4e3fd7d42 [build] Enable Zc:__cplusplus for Windows (#3625)
791d8354d [build] Suppress deprecation/removal warnings for old commands (#3618)
10f19e6fc [hal, wpilib] Add REV PneumaticsHub (#3600)
4c61a1305 [ntcore] Revert to per-element copy for toNative() (#3621)
7b3f62244 [wpiutil] SendableRegistry: Print exception stacktrace (#3620)
d347928e4 [hal] Use better error for when console out is enabled while attempting to use onboard serial port (#3622)
cc31079a1 [hal] Use setcap instead of setuid for setting thread priorities (#3613)
4676648b7 [wpimath] Upgrade to Drake v0.34.0 (#3607)
c7594c911 [build] Allow building wpilibc in cmake without cscore and opencv (#3605)
173cb7359 [wpilib] Add TimesliceRobot (#3502)
af295879f [hal] Set error status for I2C port out of range (#3603)
95dd20a15 [build] Enable spotbugs (#3601)
b65fce86b [wpilib] Remove Timer lock in wpilibj and update docs (#3602)
3b8d3bbcb Remove unused and add missing deprecated.h includes (#3599)
f9e976467 [examples] Rename DriveTrain classes to Drivetrain (#3594)
118a27be2 [wpilib] Add Timer tests (#3595)
59c89428e [wpilib] Deprecate Timer::HasPeriodPassed() (#3598)
202ca5e78 Force C++17 in .clang-format (#3597)
d6f185d8e Rename tests for consistency (#3592)
54ca474db [ci] Enable asan and tsan in CI for tests that pass (#3591)
1ca383b23 Add Debouncer (#3590)
179fde3a7 [build] Update to 2022 native utils and gradle 7 (#3588)
50198ffcf [examples] Add Mechanism2d visualization to Elevator Sim (#3587)
a446c2559 [examples] Synchronize C++ and Java Mechanism2d examples (#3589)
a7fb83103 [ci] clang-tidy: Generate compilation commands DB with Gradle (#3585)
4f5e0c9f8 [examples] Update ArmSimulation example to use Mechanism2d (#3572)
8164b91dc [CI] Print CMake test output on failure (#3583)
4d5fca27e [wpilib] Impove Mechanism2D documentation (NFC) (#3584)
fe59e4b9f Make C++ test names more consistent (#3586)
5c8868549 [wpilibc] Fix C++ MechanisimRoot2D to use same NT entries as Java/Glass (#3582)
9359431ba [wpimath] Clean up Eigen usage
72716f51c [wpimath] Upgrade to Eigen 3.4
382deef75 [wpimath] Explicitly export wpimath symbols
161e21173 [ntcore] Match standard handle layout, only allow 16 instances (#3577)
263a24811 [wpimath] Use jinja for codegen (#3574)
725251d29 [wpilib] Increase tolerances of DifferentialDriveSimTest (#3581)
4dff87301 [wpimath] Make LinearFilter::Factorial() constexpr (#3579)
60ede67ab [hal, wpilib] Switch PCM to be a single object that is allowed to be duplicated (#3475)
906bfc846 [build] Add CMake build support for sanitizers (#3576)
0d4f08ad9 [hal] Simplify string copy of joystick name (#3575)
a52bf87b7 [wpiutil] Add Java function package (#3570)
40c7645d6 [wpiutil] UidVector: Return old object from erase() (#3571)
5b886a23f [wpiutil] jni_util: Add size, operator[] to JArrayRef (#3569)
65797caa7 [sim] Fix halsim_ds_socket stringop overflow warning from GCC 10 (#3568)
66abb3988 [hal] Update runtime enum to allow selecting roborio 2 (#3565)
95a12e0ee [hal] UidSetter: Don't revert euid if its already current value (#3566)
27951442b [wpimath] Use external Eigen headers only (#3564)
c42e053ae [docs] Update to doxygen 1.9.2 (#3562)
e7048c8c8 [docs] Disable doxygen linking for common words that are also classes (#3563)
d8e0b6c97 [wpilibj] Fix java async interrupts (#3559)
5e6c34c61 Update to 2022 roborio image (#3537)
828f073eb [wpiutil] Fix uv::Buffer memory leaks caught by asan (#3555)
2dd5701ac [cscore] Fix mutex use-after-free in cscore test (#3557)
531439198 [ntcore] Fix NetworkTables memory leaks caught by asan (#3556)
3d9a4d585 [wpilibc] Fix AnalogTriggerOutput memory leak reported by asan (#3554)
54eda5928 [wpiutil] Ignore ubsan vptr upcast warning in SendableHelper moves (#3553)
5a4f75c9f [wpilib] Replace Speed controller comments with motor controller (NFC) (#3551)
7810f665f [wpiutil] Fix bug in uleb128 (#3540)
697e2dd33 [wpilib] Fix errant jaguar reference in comments (NFC) (#3550)
936c64ff5 [docs] Enable -linksource for javadocs (#3549)
1ea654954 [build] Upgrade CMake build to googletest 1.11.0 (#3548)
32d9949e4 [wpimath] Move controller tests to wpimath (#3541)
01ba56a8a [hal] Replace strncpy with memcpy (#3539)
e109c4251 [build] Rename makeSim flag to forceGazebo to better describe what it does (#3535)
e4c709164 [docs] Use a doxygen theme and add logo (#3533)
960b6e589 [wpimath] Fix Javadoc warning (#3532)
82eef8d5e [hal] Remove over current fault HAL functions from REV PDH (#3526)
aa3848b2c [wpimath] Move RobotDriveBase::ApplyDeadband() to MathUtil (#3529)
3b5d0d141 [wpimath] Add LinearFilter::BackwardFiniteDifference() (#3528)
c8fc715fe [wpimath] Upgrade drake files to v0.33.0 (#3531)
e5fe3a8e1 [build] Treat javadoc warnings as errors in CI and fix warnings (#3530)
e0c6cd3dc [wpimath] Add an operator for composing two Transform2ds (#3527)
2edd510ab [sim] Add sim wrappers for sensors that use SimDevice (#3517)
2b3e2ebc1 [hal] Fix HAL Notifier thread priority setting (#3522)
ab4cb5932 [gitignore] Update gitignore to ignore bazel / clion files (#3524)
57c8615af [build] Generate spotless patch on failure (#3523)
b90317321 Replace std::cout and std::cerr with fmt::print() (#3519)
10cc8b89c [hal] [wpilib] Add initial support for the REV PDH (#3503)
5d9ae3cdb [hal] Set HAL Notifier thread as RT by default (#3482)
192d251ee [wpilibcIntegrationTests] Properly disable DMA integration tests (#3514)
031962608 [wpilib] Add PS4Controller, remove Hand from GenericHID/XboxController (#3345)
25f6f478a [wpilib] Rename DriverStation::IsOperatorControl() to IsTeleop() (#3505)
e80f09f84 [wpilibj] Add unit tests (#3501)
c159f91f0 [wpilib] Only read DS control word once in IterativeRobotBase (#3504)
eb790a74d Add rio development docs documenting myRobot deploy tasks (#3508)
e47451f5a [wpimath] Replace auto with Eigen types (#3511)
252b8c83b Remove Java formatting from build task in CI (#3507)
09666ff29 Shorten Gazebo CI build (#3506)
baf2e501d Update myRobot to use 2021 java (#3509)
5ac60f0a2 [wpilib] Remove IterativeRobotBase mode init prints (#3500)
fb2ee8ec3 [wpilib] Add TimedRobot functions for running code on mode exit (#3499)
94e0db796 [wpilibc] Add more unit tests (#3494)
b25324695 [wpilibj] Add units to parameter names (NFC) (#3497)
1ac73a247 [hal] Rename PowerDistributionPanel to PowerDistribution (#3466)
2014115bc [examples] frisbeebot: Fix typo and reflow comments (NFC) (#3498)
4a944dc39 [examples] Consistently use 0 for controller port (#3496)
3838cc4ec Use unicode characters in docs equations (#3487)
85748f2e6 [examples] Add C++ TankDrive example (#3493)
d7b8aa56d [wpilibj] Rename DriverStation In[Mode] functions to follow style guide (#3488)
16e096cf8 [build] Fix CMake Windows CI (#3490)
50af74c38 [wpimath] Clean up NumericalIntegration and add Discretization tests (#3489)
bfc209b12 Automate fmt update (#3486)
e7f9331e4 [build] Update to Doxygen 1.9.1 (#3008)
ab8e8aa2a [wpimath] Update drake with upstream (#3484)
1ef826d1d [wpimath] Fix IOException path in WPIMath JNI (#3485)
52bddaa97 [wpimath] Disable iostream support for units and enable fmtlib (#3481)
e4dc3908b [wpiutil] Upgrade to fmtlib 8.0.1 (#3483)
1daadb812 [wpimath] Implement Dormand-Prince integration method (#3476)
9c2723391 [cscore] Add [[nodiscard]] to GrabFrame functions (#3479)
7a8796414 [wpilib] Add Notifier integration tests (#3480)
f8f13c536 [wpilibcExamples] Prefix decimal numbers with 0 (#3478)
1adb69c0f [ntcore] Use "NetworkTables" instead of "Network Tables" in NT specs (#3477)
5f5830b96 Upload wpiformat diff if one exists (#3474)
9fb4f35bb [wpimath] Add tests for DARE overload with Q, R, and N matrices (#3472)
c002e6f92 Run wpiformat (#3473)
c154e5262 [wpilib] Make solenoids exclusive use, PCM act like old sendable compressor (#3464)
6ddef1cca [hal] JNI setDIO: use a boolean and not a short (#3469)
9d68d9582 Remove extra newlines after open curly braces (NFC) (#3471)
a4233e1a1 [wpimath] Add script for updating Drake (#3470)
39373c6d2 Update README.md for new GCC version requirement (#3467)
d29acc90a [wpigui] Add option to reset UI on exit (#3463)
a371235b0 [ntcore] Fix dangling pointer in logger (#3465)
53b4891a5 [wpilibcintegrationtests] Fix deprecated Preferences usage (#3461)
646ded912 [wpimath] Remove incorrect discretization in pose estimators (#3460)
ea0b8f48e Fix some deprecation warnings due to fmtlib upgrade (#3459)
2067d7e30 [wpilibjexamples] Add wpimathjni, wpiutiljni to library path (#3455)
866571ab4 [wpiutil] Upgrade to fmtlib 8.0.0 (#3457)
4e1fa0308 [build] Skip PDB copy on windows build servers (#3458)
b45572167 [build] Change CI back to 18.04 docker images (#3456)
57a160f1b [wpilibc] Fix LiveWindow deprecation warning in RobotBase skeleton template (#3454)
29ae8640d [HLT] Implement duty cycle cross connect tests (#3453)
ee6377e54 [HLT] Add relay and analog cross connects (#3452)
b0f1ae7ea [build] CMake: Build the HAL even if WITH_CSCORE=OFF (#3449)
7aae2b72d Replace std::to_string() with fmt::format() (#3451)
73fcbbd74 [HLT] Add relay digital cross connect tests (#3450)
e7bedde83 [HLT] Add PWM tests that use DMA as the back end (#3447)
7253edb1e [wpilibc] Timer: Fix deprecated warning (#3446)
efa28125c [wpilibc] Add message to RobotBase on how to read stacktrace (#3444)
9832fcfe1 [hal] Fix DIO direction getter (#3445)
49c71f9f2 [wpilibj] Clarify robot quit message (#3364)
791770cf6 [wpimath] Move controller from wpilibj to wpimath (#3439)
9ce9188ff [wpimath] Add ReportWarning to MathShared (#3441)
362066a9b [wpilib] Deprecate getInstance() in favor of static functions (#3440)
26ff9371d Initial commit of cross connect integration test project (#3434)
4a36f86c8 [hal] Add support for DMA to Java (#3158)
85144e47f [commands] Unbreak build (#3438)
b417d961e Split Sendable into NT and non-NT portions (#3432)
ef4ea84cb [commands] Change grouping decorator impl to flatten nested group structures (#3335)
b422665a3 [examples] Invert right side of drive subsystems (#3437)
186dadf14 [hal] Error if attempting to set DIO output on an input port (#3436)
04e64db94 Remove redundant C++ lambda parentheses (NFC) (#3433)
f60994ad2 [wpiutil] Rename Java package to edu.wpi.first.util (#3431)
cfa1ca96f [wpilibc] Make ShuffleboardValue non-copyable (#3430)
4d9ff7643 Fix documentation warnings generated by JavaDoc (NFC) (#3428)
9e1b7e046 [build] Fix clang-tidy and clang-format (#3429)
a77c6ff3a [build] Upgrade clang-format and clang-tidy (NFC) (#3422)
099fde97d [wpilib] Improve PDP comments (NFC) (#3427)
f8fc2463e [wpilibc, wpiutil] Clean up includes (NFC) (#3426)
e246b7884 [wpimath] Clean up member initialization in feedforward classes (#3425)
c1e128bd5 Disable frivolous PMD warnings and enable PMD in ntcore (#3419)
8284075ee Run "Lint and Format" CI job on push as well as pull request (#3412)
f7db09a12 [wpimath] Move C++ filters into filter folder to match Java (#3417)
f9c3d54bd [wpimath] Reset error covariance in pose estimator ResetPosition() (#3418)
0773f4033 [hal] Ensure HAL status variables are initialized to zero (#3421)
d068fb321 [build] Upgrade CI to use 20.04 docker images (#3420)
8d054c940 [wpiutil] Remove STLExtras.h
80f1d7921 [wpiutil] Split function_ref to a separate header
64f541325 Use wpi::span instead of wpi::ArrayRef across all libraries (#3414)
2abbbd9e7 [build] clang-tidy: Remove bugprone-exception-escape (#3415)
a5c471af7 [wpimath] Add LQR template specialization for 2x2 system
edd2f0232 [wpimath] Add DARE solver for Q, R, and N with LQR ctor overloads
b2c3b2dd8 Use std::string_view and fmtlib across all libraries (#3402)
4f1cecb8e [wpiutil] Remove Path.h (#3413)
b336eac34 [build] Publish halsim_ws_core to Maven
2a09f6fa4 [build] Also build sim modules as static libraries
0e702eb79 [hal] Add a unified PCM object (#3331)
dea841103 [wpimath] Add fmtlib formatter overloads for Eigen::Matrix and units (#3409)
82856cf81 [wpiutil] Improve wpi::circular_buffer iterators (#3410)
8aecda03e [wpilib] Fix a documentation typo (#3408)
5c817082a [wpilib] Remove InterruptableSensorBase and replace with interrupt classes (#2410)
15c521a7f [wpimath] Fix drivetrain system identification (#3406)
989de4a1b [build] Force all linker warnings to be fatal for rio builds (#3407)
d9eeb45b0 [wpilibc] Add units to Ultrasonic class API (#3403)
fe570e000 [wpiutil] Replace llvm filesystem with C++17 filesystem (#3401)
01dc0249d [wpimath] Move SlewRateLimiter from wpilib to wpimath (#3399)
93523d572 [wpilibc] Clean up integration tests (#3400)
4f7a4464d [wpiutil] Rewrite StringExtras for std::string_view (#3394)
e09293a15 [wpilibc] Transition C++ classes to units::second_t (#3396)
827b17a52 [build] Create run tasks for Glass and OutlineViewer (#3397)
a61037996 [wpiutil] Avoid MSVC warning on span include (#3393)
4e2c3051b [wpilibc] Use std::string_view instead of Twine (#3380)
50915cb7e [wpilibc] MotorSafety::GetDescription(): Return std::string (#3390)
f4e2d26d5 [wpilibc] Move NullDeleter from frc/Base.h to wpi/NullDeleter.h (#3387)
cb0051ae6 [wpilibc] SimDeviceSim: use fmtlib (#3389)
a238cec12 [wpiutil] Deprecate wpi::math constants in favor of wpi::numbers (#3383)
393bf23c0 [ntcore, cscore, wpiutil] Standardize template impl files on .inc extension (NFC) (#3124)
e7d9ba135 [sim] Disable flaky web server integration tests (#3388)
0a0003c11 [wpilibjExamples] Fix name of Java swerve drive pose estimator example (#3382)
7e1b27554 [wpilibc] Use default copies and moves when possible (#3381)
fb2a56e2d [wpilibc] Remove START_ROBOT_CLASS macro (#3384)
84218bfb4 [wpilibc] Remove frc namespace shim (#3385)
dd7824340 [wpilibc] Remove C++ compiler version static asserts (#3386)
484cf9c0e [wpimath] Suppress the -Wmaybe-uninitialized warning in Eigen (#3378)
a04d1b4f9 [wpilibc] DriverStation: Remove ReportError and ReportWarning
831c10bdf [wpilibc] Errors: Use fmtlib
87603e400 [wpiutil] Import fmtlib (#3375)
442621672 [wpiutil] Add ArrayRef/std::span/wpi::span implicit conversions
bc15b953b [wpiutil] Add std::span implementation
6d20b1204 [wpiutil] StringRef, Twine, raw_ostream: Add std::string_view support (#3373)
2385c2a43 [wpilibc] Remove Utility.h (#3376)
87384ea68 [wpilib] Fix PIDController continuous range error calculations (#3170)
04dae799a [wpimath] Add SimpleMotorFeedforward::Calculate(velocity, nextVelocity) overload (#3183)
0768c3903 [wpilib] DifferentialDrive: Remove right side inversion (#3340)
8dd8d4d2d [wpimath] Fix redundant nested math package introduced by #3316 (#3368)
49b06beed [examples] Add Field2d to RamseteController example (#3371)
4c562a445 [wpimath] Fix typo in comment of update_eigen.py (#3369)
fdbbf1188 [wpimath] Add script for updating Eigen
f1e64b349 [wpimath] Move Eigen unsupported folder into eigeninclude
224f3a05c [sim] Fix build error when building with GCC 11.1 (#3361)
ff56d6861 [wpilibj] Fix SpeedController deprecated warnings (#3360)
1873fbefb [examples] Fix Swerve and Mecanum examples (#3359)
80b479e50 [examples] Fix SwerveBot example to use unique encoder ports (#3358)
1f7c9adee [wpilibjExamples] Fix pose estimator examples (#3356)
9ebc3b058 [outlineviewer] Change default size to 600x400 (#3353)
e21b443a4 [build] Gradle: Make C++ examples runnable (#3348)
da590120c [wpilibj] Add MotorController.setVoltage default (#3347)
561d53885 [build] Update opencv to 4.5.2, imgui/implot to latest (#3344)
44ad67ca8 [wpilibj] Preferences: Add missing Deprecated annotation (#3343)
3fe8fc75a [wpilibc] Revert "Return reference from GetInstance" (#3342)
3cc2da332 Merge branch '2022'
a3cd90dd7 [wpimath] Fix classpath used by generate_numbers.py (#3339)
d6cfdd3ba [wpilib] Preferences: Deprecate Put* in favor of Set* (#3337)
ba08baabb [wpimath] Update Drake DARE solver to v0.29.0 (#3336)
497b712f6 [wpilib] Make IterativeRobotBase::m_period private with getter
f00dfed7a [wpilib] Remove IterativeRobot base class
3c0846168 [hal] Use last error reporting instead of PARAMETER_OUT_OF_RANGE (#3328)
5ef2b4fdc [wpilibj] Fix @deprecated warning for SerialPort constructor (#3329)
23d2326d1 [hal] Report previous allocation location for indexed resource duplicates (#3322)
e338f9f19 [build] Fix wpilibc runCpp task (#3327)
c8ff626fe [wpimath] Move Java classes to edu.wpi.first.math (#3316)
4e424d51f [wpilibj] DifferentialDrivetrainSim: Rename constants to match the style guide (#3312)
6b50323b0 [cscore] Use Lock2DSize if possible for Windows USB cameras (#3326)
65c148536 [wpilibc] Fix "control reaches end of non-void function" warning (#3324)
f99f62bee [wpiutil] uv Handle: Use malloc/free instead of new/delete (#3325)
365f5449c [wpimath] Fix MecanumDriveKinematics (#3266)
ff52f207c [glass, wpilib] Rewrite Mechanism2d (#3281)
ee0eed143 [wpimath] Add DCMotor factory function for Romi motors (#3319)
512738072 [hal] Add HAL_GetLastError to enable better error messages from HAL calls (#3320)
ced654880 [glass, outlineviewer] Update Mac icons to macOS 11 style (#3313)
936d3b9f8 [templates] Add Java template for educational robot (#3309)
6e31230ad [examples] Fix odometry update in SwerveControllerCommand example (#3310)
05ebe9318 Merge branch 'main' into 2022
aaf24e255 [wpilib] Fix initial heading behavior in HolonomicDriveController (#3290)
8d961dfd2 [wpilibc] Remove ErrorBase (#3306)
659b37ef9 [wpiutil] StackTrace: Include offset on Linux (#3305)
0abf6c904 [wpilib] Move motor controllers to motorcontrol package (#3302)
4630191fa [wpiutil] circular_buffer: Use value initialization instead of passing zero (#3303)
b7b178f49 [wpilib] Remove Potentiometer interface
687066af3 [wpilib] Remove GyroBase
6b168ab0c [wpilib] Remove PIDController, PIDOutput, PIDSource
948625de9 [wpimath] Document conversion from filter cutoff frequency to time constant (#3299)
3848eb8b1 [wpilibc] Fix flywhel -> flywheel typo in FlywheelSim (#3298)
3abe0b9d4 [cscore] Move java package to edu.wpi.first.cscore (#3294)
d7fabe81f [wpilib] Remove RobotDrive (#3295)
1dc81669c [wpilib] Remove GearTooth (#3293)
01d0e1260 [wpilib] Revert move of RomiGyro into main wpilibc/j (#3296)
397e569aa [ntcore] Remove "using wpi" from nt namespace
79267f9e6 [ntcore] Remove NetworkTable -> nt::NetworkTable shim
48ebe5736 [ntcore] Remove deprecated Java interfaces and classes
c2064c78b [ntcore] Remove deprecated ITable interfaces
36608a283 [ntcore] Remove deprecated C++ APIs
a1c87e1e1 [glass] LogView: Add "copy to clipboard" button (#3274)
fa7240a50 [wpimath] Fix typo in quintic spline basis matrix
ffb4d38e2 [wpimath] Add derivation for spline basis matrices
f57c188f2 [wpilib] Add AnalogEncoder(int) ctor (#3273)
8471c4fb2 [wpilib] FieldObject2d: Add setTrajectory() method (#3277)
c97acd18e [glass] Field2d enhancements (#3234)
ffb590bfc [wpilib] Fix Compressor sendable properties (#3269)
6137f98eb [hal] Rename SimValueCallback2 to SimValueCallback (#3212)
a6f653969 [hal] Move registerSimPeriodic functions to HAL package (#3211)
10c038d9b [glass] Plot: Fix window creation after removal (#3264)
2d2eaa3ef [wpigui] Ensure window will be initially visible (#3256)
4d28b1f0c [wpimath] Use JNI for trajectory serialization (#3257)
3de800a60 [wpimath] TrajectoryUtil.h: Comment formatting (NFC) (#3262)
eff592377 [glass] Plot: Don't overwrite series ID (#3260)
a79faace1 [wpilibc] Return reference from GetInstance (#3247)
9550777b9 [wpilib] PWMSpeedController: Use PWM by composition (#3248)
c8521a3c3 [glass] Plot: Set reasonable default window size (#3261)
d71eb2cf3 [glass] Plot: Show full source name as tooltip and in popup (#3255)
160fb740f [hal] Use std::lround() instead of adding 0.5 and truncating (#3012)
48e9f3951 [wpilibj] Remove wpilibj package CameraServer (#3213)
8afa596fd [wpilib] Remove deprecated Sendable functions and SendableBase (#3210)
d3e45c297 [wpimath] Make C++ geometry classes immutable (#3249)
2c98939c1 [glass] StringChooser: Don't call SameLine() at end
a18a7409f [glass] NTStringChooser: Clear value of deleted entries
2f19cf452 [glass] NetworkTablesHelper: listen to delete events
da96707dc Merge branch 'main' into 2022
c3a8bdc24 [build] Fix clang-tidy action (#3246)
21624ef27 Add ImGui OutlineViewer (#3220)
1032c9b91 [wpiutil] Unbreak wpi::Format on Windows (#3242)
2e07902d7 [glass] NTField2D: Fix name lookup (#3233)
6e23e1840 [wpilibc] Remove WPILib.h (#3235)
3e22e4506 [wpilib] Make KoP drivetrain simulation weight 60 lbs (#3228)
79d1bd6c8 [glass] NetworkTablesSetting: Allow disable of server option (#3227)
fe341a16f [examples] Use more logical elevator setpoints in GearsBot (#3198)
62abf46b3 [glass] NetworkTablesSettings: Don't block GUI (#3226)
a95a5e0d9 [glass] Move NetworkTablesSettings to libglassnt (#3224)
d6f6ceaba [build] Run Spotless formatter (NFC) (#3221)
0922f8af5 [commands] CommandScheduler.requiring(): Note return can be null (NFC) (#2934)
6812302ff [examples] Make DriveDistanceOffboard example work in sim (#3199)
f3f86b8e7 [wpimath] Add pose estimator overload for vision + std dev measurement (#3200)
1a2680b9e [wpilibj] Change CommandBase.withName() to return CommandBase (#3209)
435bbb6a8 [command] RamseteCommand: Output 0 if interrupted (#3216)
3cf44e0a5 [hal] Add function for changing HAL Notifier thread priority (#3218)
40b367513 [wpimath] Units.java: Add kg-lb conversions (#3203)
9f563d584 [glass] NT: Fix return value in StringToDoubleArray (#3208)
af4adf537 [glass] Auto-size plots to fit window (#3193)
2560146da [sim] GUI: Add option to show prefix in Other Devices (#3186)
eae3a6397 gitignore: Ignore .cache directory (#3196)
959611420 [wpilib] Require non-zero positive value for PIDController.period (#3175)
9522f2e8c [wpimath] Add methods to concatenate trajectories (#3139)
e42a0b6cf [wpimath] Rotation2d comment formatting (NFC) (#3162)
d1c7032de [wpimath] Fix order of setting gyro offset in pose estimators (#3176)
d241bc81a [sim] Add DoubleSolenoidSim and SolenoidSim classes (#3177)
cb7f39afa [wpilibc] Add RobotController::GetBatteryVoltage() to C++ (#3179)
99b5ad9eb [wpilibj] Fix warnings that are not unused variables or deprecation (#3161)
c14b23775 [build] Fixup doxygen generated include dirs to match what users would need (#3154)
d447c7dc3 [sim] Add SimDeviceSim ctor overloads (#3134)
247420c9c [build] Remove jcenter repo (#3157)
04b112e00 [build] Include debug info in plugin published artifacts (#3149)
be0ce9900 [examples] Use PWMSparkMax instead of PWMVictorSPX (#3156)
69e8d0b65 [wpilib] Move RomiGyro into main wpilibc/j (#3143)
94e685e1b [wpimath] Add custom residual support to EKF (#3148)
5899f3dd2 [sim] GUI: Make keyboard settings loading more robust (#3167)
f82aa1d56 [wpilib] Fix HolonomicDriveController atReference() behavior (#3163)
fe5c2cf4b [wpimath] Remove ControllerUtil.java (#3169)
43d40c6e9 [wpiutil] Suppress unchecked cast in CombinedRuntimeLoader (#3155)
3d44d8f79 [wpimath] Fix argument order in UKF docs (NFC) (#3147)
ba6fe8ff2 [cscore] Add USB camera change event (#3123)
533725888 [build] Tweak OpenCV cmake search paths to work better on Linux (#3144)
29bf9d6ef [cscore] Add polled support to listener
483beb636 [ntcore] Move CallbackManager to wpiutil
fdaec7759 [examples] Instantiate m_ramseteController in example (#3142)
8494a5761 Rename default branch to main (#3140)
45590eea2 [wpigui] Hardcode window scale to 1 on macOS (#3135)
834a64920 [build] Publish libglass and libglassnt to Maven (#3127)
2c2ccb361 [wpimath] Fix Rotation2d equality operator (#3128)
fb5c8c39a [wpigui] clang-tidy: readability-braces-around-statements
f7d39193a [wpigui] Fix copyright in pfd and wpigui_metal.mm
aec796b21 [ntcore] Fix conditional jump on uninitialized value (#3125)
fb13bb239 [sim] GUI: Add right click popup for keyboard joystick settings (#3119)
c517ec677 [build] Update thirdparty-imgui to 1.79-2 (#3118)
e8cbf2a71 [wpimath] Fix typo in SwerveDrivePoseEstimator doc (NFC) (#3112)
e9c86df46 [wpimath] Add tests for swerve module optimization (#3100)
6ba8c289c [examples] Remove negative of ArcadeDrive(fwd, ..) in the C++ Getting Started Example (#3102)
3f1672e89 [hal] Add SimDevice createInt() and createLong() (#3110)
15be5cbf1 [examples] Fix segfault in GearsBot C++ example (#3111)
4cf0e5e6d Add quick links to API documentation in README (#3082)
6b1898f12 Fix RT priority docs (NFC) (#3098)
b3426e9c0 [wpimath] Fix missing whitespace in pose estimator doc (#3097)
38c1a1f3e [examples] Fix feildRelative -> fieldRelative typo in XControllerCommand examples (#3104)
4488e25f1 [glass] Shorten SmartDashboard window names (#3096)
cfdb3058e [wpilibj] Update SimDeviceSimTest (#3095)
64adff5fe [examples] Fix typo in ArcadeDrive constructor parameter name (#3092)
6efc58e3d [build] Fix issues with build on windows, deprecations, and native utils (#3090)
f393989a5 [wpimath, wpiutil] Add wpi::array for compile time size checking (#3087)
d6ed20c1e [build] Set macOS deployment target to 10.14 (#3088)
7c524014c [hal] Add [[nodiscard]] to HAL_WaitForNotifierAlarm() (#3085)
406d055f0 [wpilib] Fixup wouldHitLowerLimit in elevator and arm simulation classes. (#3076)
04a90b5dd [examples] Don't continually set setpoint in PotentiometerPID Examples (#3084)
8c5bfa013 [sim] GUI: Add max value setting for keyboard joysticks (#3083)
bc80c5535 [hal] Add SimValue reset() function (#3064)
9c3b51ca0 [wpilib] Document simulation APIs (#3079)
26584ff14 [wpimath] Add model description to LinearSystemId Javadocs (#3080)
42c3d5286 [examples] Sync Java and C++ trajectories in sim example (#3081)
64e72f710 [wpilibc] Add missing function RoboRioSim::ResetData (#3073)
e95503798 [wpimath] Add optimize() to SwerveModuleState (#3065)
fb99910c2 [hal] Add SimInt and SimLong wrappers for int/long SimValue (#3066)
e620bd4d3 [doc] Add machine-readable websocket specification (#3059)
a44e761d9 [glass] Add support for plot Y axis labels
ea1974d57 [wpigui] Update imgui and implot to latest
85a0bd43c [wpimath] Add RKF45 integration (#3047)
278e0f126 [glass] Use .controllable to set widgets' read-only state (#3035)
d8652cfd4 [wpimath] Make Java DCMotor API consistent with C++ and fix motor calcs (#3046)
377b7065a [build] Add toggleOffOn to Java spotless (#3053)
1e9c79c58 [sim] Use plant output to retrieve simulated position (#3043)
78147aa34 [sim] GUI: Fix Keyboard Joystick (#3052)
cd4a2265b [ntcore] Fix NetworkTableEntry::GetRaw() (#3051)
767ac1de1 [build] Use deploy key for doc publish (#3048)
d762215d1 [build] Add publish documentation script (#3040)
1fd09593c [examples] Add missing TestInit method to GettingStarted Example (#3039)
e45a0f6ce [examples] Add RomiGyro to the Romi Reference example (#3037)
94f852572 Update imaging link and fix typo (#3038)
d73cf64e5 [examples] Update RomiReference to match motor directions (#3036)
f945462ba Bump copyright year to 2021 (#3033)
b05946175 [wpimath] Catch Drake JNI exceptions and rethrow them (#3032)
62f0f8190 [wpimath] Deduplicate angle modulus functions (#2998)
bf8c0da4b [glass] Add "About" popup with version number (#3031)
dfdd6b389 [build] Increase Gradle heap size in Gazebo build (#3028)
f5e0fc3e9 Finish clang-tidy cleanups (#3003)
d741101fe [sim] Revert accidental commit of WSProvider_PDP.h (#3027)
e1620799c [examples] Add C++ RomiReference example (#2969)
749c7adb1 [command] Fix use-after-free in CommandScheduler (#3024)
921a73391 [sim] Add WS providers for AddressableLED, PCM, and Solenoid (#3026)
26d0004fe [build] Split Actions into different yml files (#3025)
948af6d5b [wpilib] PWMSpeedController.get(): Apply Inversion (#3016)
670a187a3 [wpilibc] SuppliedValueWidget.h: Forward declare ShuffleboardContainer (#3021)
be9f72502 [ntcore] NetworkTableValue: Use std::forward instead of std::move (#3022)
daf3f4cb1 [cscore] cscore_raw_cv.h: Fix error in PutFrame() (#3019)
5acda4cc7 [wpimath] ElevatorFeedforward.h: Add time.h include
8452af606 [wpimath] units/radiation.h: Add mass.h include
630d44952 [hal] ErrorsInternal.h: Add stdint.h include
7372cf7d9 [cscore] Windows NetworkUtil.cpp: Add missing include
b7e46c558 Include .h from .inc/.inl files (NFC) (#3017)
bf8f8710e [examples] Update Romi template and example (#2996)
6ffe5b775 [glass] Ensure NetworkTableTree parent context menu has an id (#3015)
be0805b85 [build] Update to WPILibVersioningPlugin 4.1.0 (#3014)
65b2359b2 [build] Add spotless for other files (#3007)
8651aa73e [examples] Enable NT Flush in Field2d examples (#3013)
78b542737 [build] Add Gazebo build to Actions CI (#3004)
fccf86532 [sim] DriverStationGui: Fix two bugs (#3010)
185741760 [sim] WSProvider_Joystick: Fix off-by-1 in incoming buttons (#3011)
ee7114a58 [glass] Add drive class widgets (#2975)
00fa91d0d [glass] Use ImGui style for gyro widget colors (#3009)
b7a25bfc3 ThirdPartyNotices: Add portable file dialogs license (#3005)
a2e46b9a1 [glass] modernize-use-nullptr (NFC) (#3006)
a751fa22d [build] Apply spotless for java formatting (#1768)
e563a0b7d [wpimath] Make LinearSystemLoop move-constructible and move-assignable (#2967)
49085ca94 [glass] Add context menus to remove and add NetworkTables values (#2979)
560a850a2 [glass] Add NetworkTables Log window (#2997)
66782e231 [sim] Create Left/Right drivetrain current accessors (#3001)
b60eb1544 clang-tidy: bugprone-virtual-near-miss
cbe59fa3b clang-tidy: google-explicit-constructor
c97c6dc06 clang-tidy: google-readability-casting (NFC)
32fa97d68 clang-tidy: modernize-use-nullptr (NFC)
aee460326 clang-tidy: modernize-pass-by-value
29c7da5f1 clang-tidy: modernize-make-unique
6131f4e32 clang-tidy: modernize-concat-nested-namespaces (NFC)
67e03e625 clang-tidy: modernize-use-equals-default
b124f9101 clang-tidy: modernize-use-default-member-init
d11a3a638 clang-tidy: modernize-use-override (NFC)
4cc0706b0 clang-tidy: modernize-use-using (NFC)
885f5a978 [wpilibc] Speed up ScopedTracerTest (#2999)
60b596457 [wpilibj] Fix typos (NFC) (#3000)
6e1919414 [build] Bring naming checkstyle rules up to date with Google Style guide (#1781)
8c8ec5e63 [wpilibj] Suppress unchecked cast warnings (#2995)
b8413ddd5 [wpiutil] Add noexcept to timestamp static functions (#2994)
5d976b6e1 [glass] Load NetworkTableView settings on first draw (#2993)
2b4317452 Replace NOLINT(runtime/explicit) comments with NOLINT (NFC) (#2992)
1c3011ba4 [glass] Fix handling of "/" NetworkTables key (#2991)
574a42f3b [hal] Fix UnsafeManipulateDIO status check (#2987)
9005cd59e [wpilib] Clamp input voltage in sim classes (#2955)
dd494d4ab [glass] NetworkTablesModel::Update(): Avoid use-after-move (#2988)
7cca469a1 [wpimath] NormalizeAngle: Make inline, remove unnamed namespace (#2986)
2aed432b4 Add braces to C++ single-line loops and conditionals (NFC) (#2973)
0291a3ff5 [wpiutil] StringRef: Add noexcept to several constructors (#2984)
5d7315280 [wpimath] Update UnitsTest.cpp copyright (#2985)
254931b9a [wpimath] Remove LinearSystem from LinearSystemLoop (#2968)
aa89744c9 Update OtherVersions.md to include wpimath info (#2983)
1cda3f5ad [glass] Fix styleguide (#2976)
8f1f64ffb Remove year from file copyright message (NFC) (#2972)
2bc0a7795 [examples] Fix wpiformat warning about utility include (#2971)
4204da6ad [glass] Add application icon
7ac39b10f [wpigui] Add icon support
6b567e006 [wpimath] Add support for varying vision standard deviations in pose estimators (#2956)
df299d6ed [wpimath] Add UnscentedKalmanFilter::Correct() overload (#2966)
4e34f0523 [examples] Use ADXRS450_GyroSim class in simulation example (#2964)
9962f6fd7 [wpilib] Give Field2d a default Sendable name (#2953)
f9d492f4b [sim] GUI: Show "Other Devices" window by default (#2961)
a8bb2ef1c [sim] Fix ADXRS450_GyroSim and DutyCycleEncoderSim (#2963)
240c629cd [sim] Try to guess "Map Gamepad" setting (#2960)
952567dd3 [wpilibc] Add missing move constructors and assignment operators (#2959)
10b396b4c [sim] Various WebSockets fixes and enhancements (#2952)
699bbe21a [examples] Fix comments in Gearsbot to match implementation (NFC) (#2957)
27b67deca [glass] Add more widgets (#2947)
581b7ec55 [wpilib] Add option to flush NetworkTables every iterative loop
acfbb1a44 [ntcore] DispatcherBase::Flush: Use wpi::Now()
d85a6d8fe [ntcore] Reduce limit on flush and update rate to 5 ms
20fbb5c63 [sim] Fix stringop truncation warning from GCC 10 (#2945)
1051a06a7 [glass] Show NT timestamps in seconds (#2944)
98dfc2620 [glass] Fix plots (#2943)
1ba0a2ced [sim] GUI: Add keyboard virtual joystick support (#2940)
4afb13f98 [examples] Replace M_PI with wpi::math::pi (#2938)
b27d33675 [examples] Enhance Romi templates (#2931)
00b9ae77f [sim] Change default WS port number to 3300 (#2932)
65219f309 [examples] Update Field2d position in periodic() (#2928)
f78d1d434 [sim] Process WS Encoder reset internally (#2927)
941edca59 [hal] Add Java SimDeviceDataJNI.getSimDeviceName (#2924)
a699435ed [wpilibj] Fix FlywheelSim argument order in constructor (#2922)
66d641718 [examples] Add tasks to run Java examples (#2920)
558e37c41 [examples] Add simple differential drive simulation example (#2918)
4f40d991e [glass] Switch name of Glass back to glass (#2919)
549af9900 [build] Update native-utils to 2021.0.6 (#2914)
b33693009 [glass] Change basename of glass to Glass (#2915)
c9a0edfb8 [glass] Package macOS application bundle
2c5668af4 [wpigui] Add platform-specific preferences save
751dea32a [wpilibc] Try to work around ABI break introduced in #2901 (#2917)
cd8f4bfb1 [build] Package up msvc runtime into maven artifact (#2913)
a6cfcc686 [wpilibc] Move SendableChooser Doxygen comments to header (NFC) (#2911)
b8c4f603d [wpimath] Upgrade to Eigen 3.3.9 (#2910)
0075e4b39 [wpilibj] Fix NPE in Field2d (#2909)
125af556c [simulation] Fix halsim_gui ntcore and wpiutil deps (#2908)
963ad5c25 [wpilib] Add noise to Differential Drive simulator (#2903)
387f56cb7 [examples] Add Romi reference Java example and templates (#2905)
b3deda38c [examples] Zero motors on disabledInit() in sim physics examples (#2906)
2a5ca7745 [glass] Add glass: an application for display of robot data
727940d84 [wpilib] Move Field2d to SmartDashboard
8cd42478e [wpilib] SendableBuilder: Make GetTable() visible
c11d34b26 [command] Use addCommands in command group templates (#2900)
339d7445b [sim] Add HAL hooks for simulationPeriodic (#2881)
d16f05f2c [wpilib] Fix SmartDashboard update order (#2896)
5427b32a4 [wpiutil] unique_function: Restrict implicit conversion (#2899)
f73701239 [ntcore] Add missing SetDefault initializer_list functions (#2898)
f5a6fc070 [sim] Add initialized flag for all solenoids on a PCM (#2897)
bdf5ba91a [wpilibj] Fix typo in ElevatorSim (#2895)
bc8f33877 [wpilib] Add pose estimators (#2867)
3413bfc06 [wpilib] PIDController: Recompute the error in AtSetpoint() (#2822)
2056f0ce0 [wpilib] Fix bugs in Hatchbot examples (#2893)
5eb8cfd69 [wpilibc] Fix MatchDataSender (#2892)
e6a425448 [build] Delete test folders after tests execute (#2891)
d478ad00d [imgui] Allow usage of imgui_stdlib (#2889)
53eda861d [build] Add unit-testing infrastructure to examples (#2863)
cc1d86ba6 [sim] Add title to simulator GUI window (#2888)
f0528f00e [build] CMake: Use project-specific binary and source dirs (#2886)
5cd2ad124 [wpilibc] Add Color::operator!= (#2887)
6c00e7a90 [build] CI CMake: build with GUI enabled (#2884)
53170bbb5 Update roboRIO toolchain installation instructions (#2883)
467258e05 [sim] GUI: Add option to not zero disconnected joysticks (#2876)
129be23c9 Clarify JDK installation instructions in readme (#2882)
8e9290e86 [build] Add separate CMake setting for wpimath (#2885)
7cf5bebf8 [wpilibj] Cache NT writes from DriverStation (#2780)
f7f9087fb [command] Fix timing issue in RamseteCommand (#2871)
256e7904f [wpilibj] SimDeviceSim: Fix sim value changed callback (#2880)
c8ea1b6c3 [wpilib] Add function to adjust LQR controller gain for pure time delay (#2878)
2816b06c0 [sim] HAL_GetControlWord: Fully zero output (#2873)
4c695ea08 Add toolchain installation instructions to README (#2875)
a14d51806 [wpimath] DCMotor: fix doc typo (NFC) (#2868)
017097791 [build] CMake: build sim extensions as shared libs (#2866)
f61726b5a [build] Fix cmake-config files (#2865)
fc27fdac5 [wpilibc] Cache NT values from driver station (#2768)
47c59859e [sim] Make SimDevice callbacks synchronous (#2861)
6e76ab9c0 [build] Turn on WITH_GUI for Windows cmake CI
5f78b7670 [build] Set GLFW_INSTALL to OFF
5e0808c84 [wpigui] Fix Windows cmake build
508f05a47 [imgui] Fix typo in Windows CMake target sources
Change-Id: I1737b45965f31803a96676bedc7dc40e337aa321
git-subtree-dir: third_party/allwpilib
git-subtree-split: e473a00f9785f9949e5ced30901baeaf426d2fc9
Signed-off-by: Austin Schuh <austin.linux@gmail.com>
diff --git a/glass/src/lib/native/cpp/Context.cpp b/glass/src/lib/native/cpp/Context.cpp
new file mode 100644
index 0000000..936acb2
--- /dev/null
+++ b/glass/src/lib/native/cpp/Context.cpp
@@ -0,0 +1,445 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/Context.h"
+
+#include <algorithm>
+#include <cinttypes>
+#include <cstdio>
+
+#include <imgui.h>
+#include <imgui_internal.h>
+#include <imgui_stdlib.h>
+#include <wpi/StringExtras.h>
+#include <wpi/timestamp.h>
+#include <wpigui.h>
+
+#include "glass/ContextInternal.h"
+
+using namespace glass;
+
+Context* glass::gContext;
+
+static bool ConvertInt(Storage::Value* value) {
+ value->type = Storage::Value::kInt;
+ if (auto val = wpi::parse_integer<int>(value->stringVal, 10)) {
+ value->intVal = val.value();
+ return true;
+ }
+ return false;
+}
+
+static bool ConvertInt64(Storage::Value* value) {
+ value->type = Storage::Value::kInt64;
+ if (auto val = wpi::parse_integer<int64_t>(value->stringVal, 10)) {
+ value->int64Val = val.value();
+ return true;
+ }
+ return false;
+}
+
+static bool ConvertBool(Storage::Value* value) {
+ value->type = Storage::Value::kBool;
+ if (auto val = wpi::parse_integer<int>(value->stringVal, 10)) {
+ value->intVal = (val.value() != 0);
+ return true;
+ }
+ return false;
+}
+
+static bool ConvertFloat(Storage::Value* value) {
+ value->type = Storage::Value::kFloat;
+ if (auto val = wpi::parse_float<float>(value->stringVal)) {
+ value->floatVal = val.value();
+ return true;
+ }
+ return false;
+}
+
+static bool ConvertDouble(Storage::Value* value) {
+ value->type = Storage::Value::kDouble;
+ if (auto val = wpi::parse_float<double>(value->stringVal)) {
+ value->doubleVal = val.value();
+ return true;
+ }
+ return false;
+}
+
+static void* GlassStorageReadOpen(ImGuiContext*, ImGuiSettingsHandler* handler,
+ const char* name) {
+ auto ctx = static_cast<Context*>(handler->UserData);
+ auto& storage = ctx->storage[name];
+ if (!storage) {
+ storage = std::make_unique<Storage>();
+ }
+ return storage.get();
+}
+
+static void GlassStorageReadLine(ImGuiContext*, ImGuiSettingsHandler*,
+ void* entry, const char* line) {
+ auto storage = static_cast<Storage*>(entry);
+ auto [key, val] = wpi::split(line, '=');
+ auto& keys = storage->GetKeys();
+ auto& values = storage->GetValues();
+ auto it = std::find(keys.begin(), keys.end(), key);
+ if (it == keys.end()) {
+ keys.emplace_back(key);
+ values.emplace_back(std::make_unique<Storage::Value>(val));
+ } else {
+ auto& value = *values[it - keys.begin()];
+ value.stringVal = val;
+ switch (value.type) {
+ case Storage::Value::kInt:
+ ConvertInt(&value);
+ break;
+ case Storage::Value::kInt64:
+ ConvertInt64(&value);
+ break;
+ case Storage::Value::kBool:
+ ConvertBool(&value);
+ break;
+ case Storage::Value::kFloat:
+ ConvertFloat(&value);
+ break;
+ case Storage::Value::kDouble:
+ ConvertDouble(&value);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+static void GlassStorageWriteAll(ImGuiContext*, ImGuiSettingsHandler* handler,
+ ImGuiTextBuffer* out_buf) {
+ auto ctx = static_cast<Context*>(handler->UserData);
+
+ // sort for output
+ std::vector<wpi::StringMapConstIterator<std::unique_ptr<Storage>>> sorted;
+ for (auto it = ctx->storage.begin(); it != ctx->storage.end(); ++it) {
+ sorted.emplace_back(it);
+ }
+ std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) {
+ return a->getKey() < b->getKey();
+ });
+
+ for (auto&& entryIt : sorted) {
+ auto& entry = *entryIt;
+ out_buf->append("[GlassStorage][");
+ out_buf->append(entry.first().data(),
+ entry.first().data() + entry.first().size());
+ out_buf->append("]\n");
+ auto& keys = entry.second->GetKeys();
+ auto& values = entry.second->GetValues();
+ for (size_t i = 0; i < keys.size(); ++i) {
+ out_buf->append(keys[i].data(), keys[i].data() + keys[i].size());
+ out_buf->append("=");
+ auto& value = *values[i];
+ switch (value.type) {
+ case Storage::Value::kInt:
+ out_buf->appendf("%d\n", value.intVal);
+ break;
+ case Storage::Value::kInt64:
+ out_buf->appendf("%" PRId64 "\n", value.int64Val);
+ break;
+ case Storage::Value::kBool:
+ out_buf->appendf("%d\n", value.boolVal ? 1 : 0);
+ break;
+ case Storage::Value::kFloat:
+ out_buf->appendf("%f\n", value.floatVal);
+ break;
+ case Storage::Value::kDouble:
+ out_buf->appendf("%f\n", value.doubleVal);
+ break;
+ case Storage::Value::kNone:
+ case Storage::Value::kString:
+ out_buf->append(value.stringVal.data(),
+ value.stringVal.data() + value.stringVal.size());
+ out_buf->append("\n");
+ break;
+ }
+ }
+ out_buf->append("\n");
+ }
+}
+
+static void Initialize(Context* ctx) {
+ wpi::gui::AddInit([=] {
+ ImGuiSettingsHandler ini_handler;
+ ini_handler.TypeName = "GlassStorage";
+ ini_handler.TypeHash = ImHashStr("GlassStorage");
+ ini_handler.ReadOpenFn = GlassStorageReadOpen;
+ ini_handler.ReadLineFn = GlassStorageReadLine;
+ ini_handler.WriteAllFn = GlassStorageWriteAll;
+ ini_handler.UserData = ctx;
+ ImGui::GetCurrentContext()->SettingsHandlers.push_back(ini_handler);
+
+ ctx->sources.Initialize();
+ });
+}
+
+static void Shutdown(Context* ctx) {}
+
+Context* glass::CreateContext() {
+ Context* ctx = new Context;
+ if (!gContext) {
+ SetCurrentContext(ctx);
+ }
+ Initialize(ctx);
+ return ctx;
+}
+
+void glass::DestroyContext(Context* ctx) {
+ if (!ctx) {
+ ctx = gContext;
+ }
+ Shutdown(ctx);
+ if (gContext == ctx) {
+ SetCurrentContext(nullptr);
+ }
+ delete ctx;
+}
+
+Context* glass::GetCurrentContext() {
+ return gContext;
+}
+
+void glass::SetCurrentContext(Context* ctx) {
+ gContext = ctx;
+}
+
+void glass::ResetTime() {
+ gContext->zeroTime = wpi::Now();
+}
+
+uint64_t glass::GetZeroTime() {
+ return gContext->zeroTime;
+}
+
+Storage::Value& Storage::GetValue(std::string_view key) {
+ auto it = std::find(m_keys.begin(), m_keys.end(), key);
+ if (it == m_keys.end()) {
+ m_keys.emplace_back(key);
+ m_values.emplace_back(std::make_unique<Value>());
+ return *m_values.back();
+ } else {
+ return *m_values[it - m_keys.begin()];
+ }
+}
+
+#define DEFUN(CapsName, LowerName, CType) \
+ CType Storage::Get##CapsName(std::string_view key, CType defaultVal) const { \
+ auto it = std::find(m_keys.begin(), m_keys.end(), key); \
+ if (it == m_keys.end()) \
+ return defaultVal; \
+ Value& value = *m_values[it - m_keys.begin()]; \
+ if (value.type != Value::k##CapsName) { \
+ if (!Convert##CapsName(&value)) \
+ value.LowerName##Val = defaultVal; \
+ } \
+ return value.LowerName##Val; \
+ } \
+ \
+ void Storage::Set##CapsName(std::string_view key, CType val) { \
+ auto it = std::find(m_keys.begin(), m_keys.end(), key); \
+ if (it == m_keys.end()) { \
+ m_keys.emplace_back(key); \
+ m_values.emplace_back(std::make_unique<Value>()); \
+ m_values.back()->type = Value::k##CapsName; \
+ m_values.back()->LowerName##Val = val; \
+ } else { \
+ Value& value = *m_values[it - m_keys.begin()]; \
+ value.type = Value::k##CapsName; \
+ value.LowerName##Val = val; \
+ } \
+ } \
+ \
+ CType* Storage::Get##CapsName##Ref(std::string_view key, CType defaultVal) { \
+ auto it = std::find(m_keys.begin(), m_keys.end(), key); \
+ if (it == m_keys.end()) { \
+ m_keys.emplace_back(key); \
+ m_values.emplace_back(std::make_unique<Value>()); \
+ m_values.back()->type = Value::k##CapsName; \
+ m_values.back()->LowerName##Val = defaultVal; \
+ return &m_values.back()->LowerName##Val; \
+ } else { \
+ Value& value = *m_values[it - m_keys.begin()]; \
+ if (value.type != Value::k##CapsName) { \
+ if (!Convert##CapsName(&value)) \
+ value.LowerName##Val = defaultVal; \
+ } \
+ return &value.LowerName##Val; \
+ } \
+ }
+
+DEFUN(Int, int, int)
+DEFUN(Int64, int64, int64_t)
+DEFUN(Bool, bool, bool)
+DEFUN(Float, float, float)
+DEFUN(Double, double, double)
+
+std::string Storage::GetString(std::string_view key,
+ std::string_view defaultVal) const {
+ auto it = std::find(m_keys.begin(), m_keys.end(), key);
+ if (it == m_keys.end()) {
+ return std::string{defaultVal};
+ }
+ Value& value = *m_values[it - m_keys.begin()];
+ value.type = Value::kString;
+ return value.stringVal;
+}
+
+void Storage::SetString(std::string_view key, std::string_view val) {
+ auto it = std::find(m_keys.begin(), m_keys.end(), key);
+ if (it == m_keys.end()) {
+ m_keys.emplace_back(key);
+ m_values.emplace_back(std::make_unique<Value>(val));
+ m_values.back()->type = Value::kString;
+ } else {
+ Value& value = *m_values[it - m_keys.begin()];
+ value.type = Value::kString;
+ value.stringVal = val;
+ }
+}
+
+std::string* Storage::GetStringRef(std::string_view key,
+ std::string_view defaultVal) {
+ auto it = std::find(m_keys.begin(), m_keys.end(), key);
+ if (it == m_keys.end()) {
+ m_keys.emplace_back(key);
+ m_values.emplace_back(std::make_unique<Value>(defaultVal));
+ m_values.back()->type = Value::kString;
+ return &m_values.back()->stringVal;
+ } else {
+ Value& value = *m_values[it - m_keys.begin()];
+ value.type = Value::kString;
+ return &value.stringVal;
+ }
+}
+
+Storage& glass::GetStorage() {
+ auto& storage = gContext->storage[gContext->curId];
+ if (!storage) {
+ storage = std::make_unique<Storage>();
+ }
+ return *storage;
+}
+
+Storage& glass::GetStorage(std::string_view id) {
+ auto& storage = gContext->storage[id];
+ if (!storage) {
+ storage = std::make_unique<Storage>();
+ }
+ return *storage;
+}
+
+static void PushIDStack(std::string_view label_id) {
+ gContext->idStack.emplace_back(gContext->curId.size());
+
+ auto [label, id] = wpi::split(label_id, "###");
+ // if no ###id, use label as id
+ if (id.empty()) {
+ id = label;
+ }
+ if (!gContext->curId.empty()) {
+ gContext->curId += "###";
+ }
+ gContext->curId += id;
+}
+
+static void PopIDStack() {
+ gContext->curId.resize(gContext->idStack.back());
+ gContext->idStack.pop_back();
+}
+
+bool glass::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) {
+ PushIDStack(name);
+ return ImGui::Begin(name, p_open, flags);
+}
+
+void glass::End() {
+ ImGui::End();
+ PopIDStack();
+}
+
+bool glass::BeginChild(const char* str_id, const ImVec2& size, bool border,
+ ImGuiWindowFlags flags) {
+ PushIDStack(str_id);
+ return ImGui::BeginChild(str_id, size, border, flags);
+}
+
+void glass::EndChild() {
+ ImGui::EndChild();
+ PopIDStack();
+}
+
+bool glass::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) {
+ wpi::SmallString<64> openKey;
+ auto [name, id] = wpi::split(label, "###");
+ // if no ###id, use name as id
+ if (id.empty()) {
+ id = name;
+ }
+ openKey = id;
+ openKey += "###open";
+
+ bool* open = GetStorage().GetBoolRef(openKey.str());
+ *open = ImGui::CollapsingHeader(
+ label, flags | (*open ? ImGuiTreeNodeFlags_DefaultOpen : 0));
+ return *open;
+}
+
+bool glass::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) {
+ PushIDStack(label);
+ bool* open = GetStorage().GetBoolRef("open");
+ *open = ImGui::TreeNodeEx(
+ label, flags | (*open ? ImGuiTreeNodeFlags_DefaultOpen : 0));
+ if (!*open) {
+ PopIDStack();
+ }
+ return *open;
+}
+
+void glass::TreePop() {
+ ImGui::TreePop();
+ PopIDStack();
+}
+
+void glass::PushID(const char* str_id) {
+ PushIDStack(str_id);
+ ImGui::PushID(str_id);
+}
+
+void glass::PushID(const char* str_id_begin, const char* str_id_end) {
+ PushIDStack(std::string_view(str_id_begin, str_id_end - str_id_begin));
+ ImGui::PushID(str_id_begin, str_id_end);
+}
+
+void glass::PushID(int int_id) {
+ char buf[16];
+ std::snprintf(buf, sizeof(buf), "%d", int_id);
+ PushIDStack(buf);
+ ImGui::PushID(int_id);
+}
+
+void glass::PopID() {
+ ImGui::PopID();
+ PopIDStack();
+}
+
+bool glass::PopupEditName(const char* label, std::string* name) {
+ bool rv = false;
+ if (ImGui::BeginPopupContextItem(label)) {
+ ImGui::Text("Edit name:");
+ if (ImGui::InputText("##editname", name)) {
+ rv = true;
+ }
+ if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
+ ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::EndPopup();
+ }
+ return rv;
+}
diff --git a/glass/src/lib/native/cpp/DataSource.cpp b/glass/src/lib/native/cpp/DataSource.cpp
new file mode 100644
index 0000000..adab6e7
--- /dev/null
+++ b/glass/src/lib/native/cpp/DataSource.cpp
@@ -0,0 +1,156 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/DataSource.h"
+
+#include <fmt/format.h>
+
+#include "glass/ContextInternal.h"
+
+using namespace glass;
+
+wpi::sig::Signal<const char*, DataSource*> DataSource::sourceCreated;
+
+DataSource::DataSource(std::string_view id) : m_id{id} {
+ auto it = gContext->sources.try_emplace(m_id, this);
+ auto& srcName = it.first->getValue();
+ m_name = srcName.name.get();
+ if (!srcName.source) {
+ srcName.source = this;
+ }
+ sourceCreated(m_id.c_str(), this);
+}
+
+DataSource::DataSource(std::string_view id, int index)
+ : DataSource{fmt::format("{}[{}]", id, index)} {}
+
+DataSource::DataSource(std::string_view id, int index, int index2)
+ : DataSource{fmt::format("{}[{},{}]", id, index, index2)} {}
+
+DataSource::~DataSource() {
+ if (!gContext) {
+ return;
+ }
+ auto it = gContext->sources.find(m_id);
+ if (it == gContext->sources.end()) {
+ return;
+ }
+ auto& srcName = it->getValue();
+ if (srcName.source == this) {
+ srcName.source = nullptr;
+ }
+}
+
+void DataSource::SetName(std::string_view name) {
+ m_name->SetName(name);
+}
+
+const char* DataSource::GetName() const {
+ return m_name->GetName();
+}
+
+void DataSource::PushEditNameId(int index) {
+ m_name->PushEditNameId(index);
+}
+
+void DataSource::PushEditNameId(const char* name) {
+ m_name->PushEditNameId(name);
+}
+
+bool DataSource::PopupEditName(int index) {
+ return m_name->PopupEditName(index);
+}
+
+bool DataSource::PopupEditName(const char* name) {
+ return m_name->PopupEditName(name);
+}
+
+bool DataSource::InputTextName(const char* label_id,
+ ImGuiInputTextFlags flags) {
+ return m_name->InputTextName(label_id, flags);
+}
+
+void DataSource::LabelText(const char* label, const char* fmt, ...) const {
+ va_list args;
+ va_start(args, fmt);
+ LabelTextV(label, fmt, args);
+ va_end(args);
+}
+
+// Add a label+text combo aligned to other label+value widgets
+void DataSource::LabelTextV(const char* label, const char* fmt,
+ va_list args) const {
+ ImGui::PushID(label);
+ ImGui::LabelTextV("##input", fmt, args);
+ ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
+ ImGui::Selectable(label);
+ ImGui::PopID();
+ EmitDrag();
+}
+
+bool DataSource::Combo(const char* label, int* current_item,
+ const char* const items[], int items_count,
+ int popup_max_height_in_items) const {
+ ImGui::PushID(label);
+ bool rv = ImGui::Combo("##input", current_item, items, items_count,
+ popup_max_height_in_items);
+ ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
+ ImGui::Selectable(label);
+ EmitDrag();
+ ImGui::PopID();
+ return rv;
+}
+
+bool DataSource::SliderFloat(const char* label, float* v, float v_min,
+ float v_max, const char* format,
+ float power) const {
+ ImGui::PushID(label);
+ bool rv = ImGui::SliderFloat("##input", v, v_min, v_max, format, power);
+ ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
+ ImGui::Selectable(label);
+ EmitDrag();
+ ImGui::PopID();
+ return rv;
+}
+
+bool DataSource::InputDouble(const char* label, double* v, double step,
+ double step_fast, const char* format,
+ ImGuiInputTextFlags flags) const {
+ ImGui::PushID(label);
+ bool rv = ImGui::InputDouble("##input", v, step, step_fast, format, flags);
+ ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
+ ImGui::Selectable(label);
+ EmitDrag();
+ ImGui::PopID();
+ return rv;
+}
+
+bool DataSource::InputInt(const char* label, int* v, int step, int step_fast,
+ ImGuiInputTextFlags flags) const {
+ ImGui::PushID(label);
+ bool rv = ImGui::InputInt("##input", v, step, step_fast, flags);
+ ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
+ ImGui::Selectable(label);
+ EmitDrag();
+ ImGui::PopID();
+ return rv;
+}
+
+void DataSource::EmitDrag(ImGuiDragDropFlags flags) const {
+ if (ImGui::BeginDragDropSource(flags)) {
+ auto self = this;
+ ImGui::SetDragDropPayload("DataSource", &self, sizeof(self)); // NOLINT
+ const char* name = GetName();
+ ImGui::TextUnformatted(name[0] == '\0' ? m_id.c_str() : name);
+ ImGui::EndDragDropSource();
+ }
+}
+
+DataSource* DataSource::Find(std::string_view id) {
+ auto it = gContext->sources.find(id);
+ if (it == gContext->sources.end()) {
+ return nullptr;
+ }
+ return it->getValue().source;
+}
diff --git a/glass/src/lib/native/cpp/MainMenuBar.cpp b/glass/src/lib/native/cpp/MainMenuBar.cpp
new file mode 100644
index 0000000..2c4d371
--- /dev/null
+++ b/glass/src/lib/native/cpp/MainMenuBar.cpp
@@ -0,0 +1,57 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/MainMenuBar.h"
+
+#include <cstdio>
+
+#include <wpigui.h>
+
+using namespace glass;
+
+void MainMenuBar::AddMainMenu(std::function<void()> menu) {
+ if (menu) {
+ m_menus.emplace_back(std::move(menu));
+ }
+}
+
+void MainMenuBar::AddOptionMenu(std::function<void()> menu) {
+ if (menu) {
+ m_optionMenus.emplace_back(std::move(menu));
+ }
+}
+
+void MainMenuBar::Display() {
+ ImGui::BeginMainMenuBar();
+
+ if (!m_optionMenus.empty()) {
+ if (ImGui::BeginMenu("Options")) {
+ for (auto&& menu : m_optionMenus) {
+ if (menu) {
+ menu();
+ }
+ }
+ ImGui::EndMenu();
+ }
+ }
+
+ wpi::gui::EmitViewMenu();
+
+ for (auto&& menu : m_menus) {
+ if (menu) {
+ menu();
+ }
+ }
+
+#if 0
+ char str[64];
+ std::snprintf(str, sizeof(str), "%.3f ms/frame (%.1f FPS)",
+ 1000.0f / ImGui::GetIO().Framerate,
+ ImGui::GetIO().Framerate);
+ ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(str).x -
+ 10);
+ ImGui::Text("%s", str);
+#endif
+ ImGui::EndMainMenuBar();
+}
diff --git a/glass/src/lib/native/cpp/Model.cpp b/glass/src/lib/native/cpp/Model.cpp
new file mode 100644
index 0000000..bee9086
--- /dev/null
+++ b/glass/src/lib/native/cpp/Model.cpp
@@ -0,0 +1,11 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/Model.h"
+
+using namespace glass;
+
+bool Model::IsReadOnly() {
+ return false;
+}
diff --git a/glass/src/lib/native/cpp/View.cpp b/glass/src/lib/native/cpp/View.cpp
new file mode 100644
index 0000000..e01c4df
--- /dev/null
+++ b/glass/src/lib/native/cpp/View.cpp
@@ -0,0 +1,27 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/View.h"
+
+using namespace glass;
+
+namespace {
+class FunctionView : public View {
+ public:
+ explicit FunctionView(wpi::unique_function<void()> display)
+ : m_display(std::move(display)) {}
+
+ void Display() override { m_display(); }
+
+ private:
+ wpi::unique_function<void()> m_display;
+};
+} // namespace
+
+std::unique_ptr<View> glass::MakeFunctionView(
+ wpi::unique_function<void()> display) {
+ return std::make_unique<FunctionView>(std::move(display));
+}
+
+void View::Hidden() {}
diff --git a/glass/src/lib/native/cpp/Window.cpp b/glass/src/lib/native/cpp/Window.cpp
new file mode 100644
index 0000000..5c014eb
--- /dev/null
+++ b/glass/src/lib/native/cpp/Window.cpp
@@ -0,0 +1,111 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/Window.h"
+
+#include <imgui_internal.h>
+#include <wpi/StringExtras.h>
+
+#include "glass/Context.h"
+
+using namespace glass;
+
+void Window::SetVisibility(Visibility visibility) {
+ switch (visibility) {
+ case kHide:
+ m_visible = false;
+ m_enabled = true;
+ break;
+ case kShow:
+ m_visible = true;
+ m_enabled = true;
+ break;
+ case kDisabled:
+ m_enabled = false;
+ break;
+ }
+}
+
+void Window::Display() {
+ if (!m_view) {
+ return;
+ }
+ if (!m_visible || !m_enabled) {
+ PushID(m_id);
+ m_view->Hidden();
+ PopID();
+ return;
+ }
+
+ if (m_posCond != 0) {
+ ImGui::SetNextWindowPos(m_pos, m_posCond);
+ }
+ if (m_sizeCond != 0) {
+ ImGui::SetNextWindowSize(m_size, m_sizeCond);
+ }
+ if (m_setPadding) {
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, m_padding);
+ }
+
+ char label[128];
+ std::snprintf(label, sizeof(label), "%s###%s",
+ m_name.empty() ? m_defaultName.c_str() : m_name.c_str(),
+ m_id.c_str());
+
+ if (Begin(label, &m_visible, m_flags)) {
+ if (m_renamePopupEnabled) {
+ PopupEditName(nullptr, &m_name);
+ }
+ m_view->Display();
+ } else {
+ m_view->Hidden();
+ }
+ End();
+ if (m_setPadding) {
+ ImGui::PopStyleVar();
+ }
+}
+
+bool Window::DisplayMenuItem(const char* label) {
+ bool wasVisible = m_visible;
+ ImGui::MenuItem(
+ label ? label : (m_name.empty() ? m_id.c_str() : m_name.c_str()), nullptr,
+ &m_visible, m_enabled);
+ return !wasVisible && m_visible;
+}
+
+void Window::ScaleDefault(float scale) {
+ if ((m_posCond & ImGuiCond_FirstUseEver) != 0) {
+ m_pos.x *= scale;
+ m_pos.y *= scale;
+ }
+ if ((m_sizeCond & ImGuiCond_FirstUseEver) != 0) {
+ m_size.x *= scale;
+ m_size.y *= scale;
+ }
+}
+
+void Window::IniReadLine(const char* line) {
+ auto [name, value] = wpi::split(line, '=');
+ name = wpi::trim(name);
+ value = wpi::trim(value);
+
+ if (name == "name") {
+ m_name = value;
+ } else if (name == "visible") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_visible = num.value();
+ }
+ } else if (name == "enabled") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_enabled = num.value();
+ }
+ }
+}
+
+void Window::IniWriteAll(const char* typeName, ImGuiTextBuffer* out_buf) {
+ out_buf->appendf("[%s][%s]\nname=%s\nvisible=%d\nenabled=%d\n\n", typeName,
+ m_id.c_str(), m_name.c_str(), m_visible ? 1 : 0,
+ m_enabled ? 1 : 0);
+}
diff --git a/glass/src/lib/native/cpp/WindowManager.cpp b/glass/src/lib/native/cpp/WindowManager.cpp
new file mode 100644
index 0000000..037b9bd
--- /dev/null
+++ b/glass/src/lib/native/cpp/WindowManager.cpp
@@ -0,0 +1,110 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/WindowManager.h"
+
+#include <algorithm>
+#include <cstdio>
+
+#include <fmt/format.h>
+#include <wpigui.h>
+
+using namespace glass;
+
+WindowManager::WindowManager(std::string_view iniName)
+ : m_iniSaver{iniName, this} {}
+
+// read/write open state to ini file
+void* WindowManager::IniSaver::IniReadOpen(const char* name) {
+ return m_manager->GetOrAddWindow(name, true);
+}
+
+void WindowManager::IniSaver::IniReadLine(void* entry, const char* lineStr) {
+ static_cast<Window*>(entry)->IniReadLine(lineStr);
+}
+
+void WindowManager::IniSaver::IniWriteAll(ImGuiTextBuffer* out_buf) {
+ const char* typeName = GetTypeName();
+ for (auto&& window : m_manager->m_windows) {
+ window->IniWriteAll(typeName, out_buf);
+ }
+}
+
+Window* WindowManager::AddWindow(std::string_view id,
+ wpi::unique_function<void()> display) {
+ auto win = GetOrAddWindow(id, false);
+ if (!win) {
+ return nullptr;
+ }
+ if (win->HasView()) {
+ fmt::print(stderr, "GUI: ignoring duplicate window '{}'\n", id);
+ return nullptr;
+ }
+ win->SetView(MakeFunctionView(std::move(display)));
+ return win;
+}
+
+Window* WindowManager::AddWindow(std::string_view id,
+ std::unique_ptr<View> view) {
+ auto win = GetOrAddWindow(id, false);
+ if (!win) {
+ return nullptr;
+ }
+ if (win->HasView()) {
+ fmt::print(stderr, "GUI: ignoring duplicate window '{}'\n", id);
+ return nullptr;
+ }
+ win->SetView(std::move(view));
+ return win;
+}
+
+Window* WindowManager::GetOrAddWindow(std::string_view id, bool duplicateOk) {
+ // binary search
+ auto it = std::lower_bound(
+ m_windows.begin(), m_windows.end(), id,
+ [](const auto& elem, std::string_view s) { return elem->GetId() < s; });
+ if (it != m_windows.end() && (*it)->GetId() == id) {
+ if (!duplicateOk) {
+ fmt::print(stderr, "GUI: ignoring duplicate window '{}'\n", id);
+ return nullptr;
+ }
+ return it->get();
+ }
+ // insert before (keeps sort)
+ return m_windows.emplace(it, std::make_unique<Window>(id))->get();
+}
+
+Window* WindowManager::GetWindow(std::string_view id) {
+ // binary search
+ auto it = std::lower_bound(
+ m_windows.begin(), m_windows.end(), id,
+ [](const auto& elem, std::string_view s) { return elem->GetId() < s; });
+ if (it == m_windows.end() || (*it)->GetId() != id) {
+ return nullptr;
+ }
+ return it->get();
+}
+
+void WindowManager::GlobalInit() {
+ wpi::gui::AddInit([this] { m_iniSaver.Initialize(); });
+ wpi::gui::AddWindowScaler([this](float scale) {
+ // scale default window positions
+ for (auto&& window : m_windows) {
+ window->ScaleDefault(scale);
+ }
+ });
+ wpi::gui::AddLateExecute([this] { DisplayWindows(); });
+}
+
+void WindowManager::DisplayMenu() {
+ for (auto&& window : m_windows) {
+ window->DisplayMenuItem();
+ }
+}
+
+void WindowManager::DisplayWindows() {
+ for (auto&& window : m_windows) {
+ window->Display();
+ }
+}
diff --git a/glass/src/lib/native/cpp/hardware/Accelerometer.cpp b/glass/src/lib/native/cpp/hardware/Accelerometer.cpp
new file mode 100644
index 0000000..6a1cc03
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/Accelerometer.cpp
@@ -0,0 +1,50 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/Accelerometer.h"
+
+#include "glass/DataSource.h"
+#include "glass/other/DeviceTree.h"
+
+using namespace glass;
+
+void glass::DisplayAccelerometerDevice(AccelerometerModel* model) {
+ if (!model->Exists()) {
+ return;
+ }
+ if (BeginDevice("BuiltInAccel")) {
+ // Range
+ {
+ int value = model->GetRange();
+ static const char* rangeOptions[] = {"2G", "4G", "8G"};
+ DeviceEnum("Range", true, &value, rangeOptions, 3);
+ }
+
+ // X Accel
+ if (auto xData = model->GetXData()) {
+ double value = xData->GetValue();
+ if (DeviceDouble("X Accel", false, &value, xData)) {
+ model->SetX(value);
+ }
+ }
+
+ // Y Accel
+ if (auto yData = model->GetYData()) {
+ double value = yData->GetValue();
+ if (DeviceDouble("Y Accel", false, &value, yData)) {
+ model->SetY(value);
+ }
+ }
+
+ // Z Accel
+ if (auto zData = model->GetZData()) {
+ double value = zData->GetValue();
+ if (DeviceDouble("Z Accel", false, &value, zData)) {
+ model->SetZ(value);
+ }
+ }
+
+ EndDevice();
+ }
+}
diff --git a/glass/src/lib/native/cpp/hardware/AnalogGyro.cpp b/glass/src/lib/native/cpp/hardware/AnalogGyro.cpp
new file mode 100644
index 0000000..be06a71
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/AnalogGyro.cpp
@@ -0,0 +1,38 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/AnalogGyro.h"
+
+#include "glass/DataSource.h"
+#include "glass/other/DeviceTree.h"
+
+using namespace glass;
+
+void glass::DisplayAnalogGyroDevice(AnalogGyroModel* model, int index) {
+ char name[32];
+ std::snprintf(name, sizeof(name), "AnalogGyro[%d]", index);
+ if (BeginDevice(name)) {
+ // angle
+ if (auto angleData = model->GetAngleData()) {
+ double value = angleData->GetValue();
+ if (DeviceDouble("Angle", false, &value, angleData)) {
+ model->SetAngle(value);
+ }
+ }
+
+ // rate
+ if (auto rateData = model->GetRateData()) {
+ double value = rateData->GetValue();
+ if (DeviceDouble("Rate", false, &value, rateData)) {
+ model->SetRate(value);
+ }
+ }
+ EndDevice();
+ }
+}
+
+void glass::DisplayAnalogGyrosDevice(AnalogGyrosModel* model) {
+ model->ForEachAnalogGyro(
+ [&](AnalogGyroModel& gyro, int i) { DisplayAnalogGyroDevice(&gyro, i); });
+}
diff --git a/glass/src/lib/native/cpp/hardware/AnalogInput.cpp b/glass/src/lib/native/cpp/hardware/AnalogInput.cpp
new file mode 100644
index 0000000..b9699a4
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/AnalogInput.cpp
@@ -0,0 +1,70 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/AnalogInput.h"
+
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void glass::DisplayAnalogInput(AnalogInputModel* model, int index) {
+ auto voltageData = model->GetVoltageData();
+ if (!voltageData) {
+ return;
+ }
+
+ // build label
+ std::string* name = GetStorage().GetStringRef("name");
+ char label[128];
+ if (!name->empty()) {
+ std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), index);
+ } else {
+ std::snprintf(label, sizeof(label), "In[%d]###name", index);
+ }
+
+ if (model->IsGyro()) {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::LabelText(label, "AnalogGyro[%d]", index);
+ ImGui::PopStyleColor();
+ } else if (auto simDevice = model->GetSimDevice()) {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::LabelText(label, "%s", simDevice);
+ ImGui::PopStyleColor();
+ } else {
+ float val = voltageData->GetValue();
+ if (voltageData->SliderFloat(label, &val, 0.0, 5.0)) {
+ model->SetVoltage(val);
+ }
+ }
+
+ // context menu to change name
+ if (PopupEditName("name", name)) {
+ voltageData->SetName(name->c_str());
+ }
+}
+
+void glass::DisplayAnalogInputs(AnalogInputsModel* model,
+ std::string_view noneMsg) {
+ ImGui::Text("(Use Ctrl+Click to edit value)");
+ bool hasAny = false;
+ bool first = true;
+ model->ForEachAnalogInput([&](AnalogInputModel& input, int i) {
+ if (!first) {
+ ImGui::Spacing();
+ ImGui::Spacing();
+ } else {
+ first = false;
+ }
+ PushID(i);
+ DisplayAnalogInput(&input, i);
+ PopID();
+ hasAny = true;
+ });
+ if (!hasAny && !noneMsg.empty()) {
+ ImGui::TextUnformatted(noneMsg.data(), noneMsg.data() + noneMsg.size());
+ }
+}
diff --git a/glass/src/lib/native/cpp/hardware/AnalogOutput.cpp b/glass/src/lib/native/cpp/hardware/AnalogOutput.cpp
new file mode 100644
index 0000000..3a9594b
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/AnalogOutput.cpp
@@ -0,0 +1,50 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/AnalogOutput.h"
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+#include "glass/other/DeviceTree.h"
+
+using namespace glass;
+
+void glass::DisplayAnalogOutputsDevice(AnalogOutputsModel* model) {
+ int count = 0;
+ model->ForEachAnalogOutput([&](auto&, int) { ++count; });
+ if (count == 0) {
+ return;
+ }
+
+ if (BeginDevice("Analog Outputs")) {
+ model->ForEachAnalogOutput([&](auto& analogOut, int i) {
+ auto analogOutData = analogOut.GetVoltageData();
+ if (!analogOutData) {
+ return;
+ }
+ PushID(i);
+
+ // build label
+ std::string* name = GetStorage().GetStringRef("name");
+ char label[128];
+ if (!name->empty()) {
+ std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), i);
+ } else {
+ std::snprintf(label, sizeof(label), "Out[%d]###name", i);
+ }
+
+ double value = analogOutData->GetValue();
+ DeviceDouble(label, true, &value, analogOutData);
+
+ if (PopupEditName("name", name)) {
+ if (analogOutData) {
+ analogOutData->SetName(name->c_str());
+ }
+ }
+ PopID();
+ });
+
+ EndDevice();
+ }
+}
diff --git a/glass/src/lib/native/cpp/hardware/DIO.cpp b/glass/src/lib/native/cpp/hardware/DIO.cpp
new file mode 100644
index 0000000..59d71f8
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/DIO.cpp
@@ -0,0 +1,121 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/DIO.h"
+
+#include <imgui.h>
+
+#include "glass/DataSource.h"
+#include "glass/hardware/Encoder.h"
+#include "glass/support/IniSaverInfo.h"
+
+using namespace glass;
+
+static void LabelSimDevice(const char* name, const char* simDeviceName) {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::LabelText(name, "%s", simDeviceName);
+ ImGui::PopStyleColor();
+}
+
+void DisplayDIOImpl(DIOModel* model, int index, bool outputsEnabled) {
+ auto dpwm = model->GetDPWM();
+ auto dutyCycle = model->GetDutyCycle();
+ auto encoder = model->GetEncoder();
+
+ auto dioData = model->GetValueData();
+ auto dpwmData = dpwm ? dpwm->GetValueData() : nullptr;
+ auto dutyCycleData = dutyCycle ? dutyCycle->GetValueData() : nullptr;
+
+ bool exists = model->Exists();
+ auto& info = dioData->GetNameInfo();
+ char label[128];
+ if (exists && dpwmData) {
+ dpwmData->GetNameInfo().GetLabel(label, sizeof(label), "PWM", index);
+ if (auto simDevice = dpwm->GetSimDevice()) {
+ LabelSimDevice(label, simDevice);
+ } else {
+ dpwmData->LabelText(label, "%0.3f", dpwmData->GetValue());
+ }
+ } else if (exists && encoder) {
+ info.GetLabel(label, sizeof(label), " In", index);
+ if (auto simDevice = encoder->GetSimDevice()) {
+ LabelSimDevice(label, simDevice);
+ } else {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::LabelText(label, "Encoder[%d,%d]", encoder->GetChannelA(),
+ encoder->GetChannelB());
+ ImGui::PopStyleColor();
+ }
+ } else if (exists && dutyCycleData) {
+ dutyCycleData->GetNameInfo().GetLabel(label, sizeof(label), "Dty", index);
+ if (auto simDevice = dutyCycle->GetSimDevice()) {
+ LabelSimDevice(label, simDevice);
+ } else {
+ double val = dutyCycleData->GetValue();
+ if (dutyCycleData->InputDouble(label, &val)) {
+ dutyCycle->SetValue(val);
+ }
+ }
+ } else {
+ const char* name = model->GetName();
+ if (name[0] != '\0') {
+ info.GetLabel(label, sizeof(label), name);
+ } else {
+ info.GetLabel(label, sizeof(label), model->IsInput() ? " In" : "Out",
+ index);
+ }
+ if (auto simDevice = model->GetSimDevice()) {
+ LabelSimDevice(label, simDevice);
+ } else {
+ if (!exists) {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ dioData->LabelText(label, "unknown");
+ ImGui::PopStyleColor();
+ } else if (model->IsReadOnly()) {
+ dioData->LabelText(
+ label, "%s",
+ outputsEnabled ? (dioData->GetValue() != 0 ? "1 (high)" : "0 (low)")
+ : "1 (disabled)");
+
+ } else {
+ static const char* options[] = {"0 (low)", "1 (high)"};
+ int val = dioData->GetValue() != 0 ? 1 : 0;
+ if (dioData->Combo(label, &val, options, 2)) {
+ model->SetValue(val);
+ }
+ }
+ }
+ }
+ if (info.PopupEditName(index)) {
+ if (dpwmData) {
+ dpwmData->SetName(info.GetName());
+ }
+ if (dutyCycleData) {
+ dutyCycleData->SetName(info.GetName());
+ }
+ }
+}
+
+void glass::DisplayDIO(DIOModel* model, int index, bool outputsEnabled) {
+ ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
+ DisplayDIOImpl(model, index, outputsEnabled);
+ ImGui::PopItemWidth();
+}
+
+void glass::DisplayDIOs(DIOsModel* model, bool outputsEnabled,
+ std::string_view noneMsg) {
+ bool hasAny = false;
+
+ ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
+ model->ForEachDIO([&](DIOModel& dio, int i) {
+ hasAny = true;
+ ImGui::PushID(i);
+ DisplayDIOImpl(&dio, i, outputsEnabled);
+ ImGui::PopID();
+ });
+ ImGui::PopItemWidth();
+ if (!hasAny && !noneMsg.empty()) {
+ ImGui::TextUnformatted(noneMsg.data(), noneMsg.data() + noneMsg.size());
+ }
+}
diff --git a/glass/src/lib/native/cpp/hardware/Encoder.cpp b/glass/src/lib/native/cpp/hardware/Encoder.cpp
new file mode 100644
index 0000000..599a5b8
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/Encoder.cpp
@@ -0,0 +1,168 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/Encoder.h"
+
+#include <fmt/format.h>
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void EncoderModel::SetName(std::string_view name) {
+ if (name.empty()) {
+ if (auto distancePerPulse = GetDistancePerPulseData()) {
+ distancePerPulse->SetName("");
+ }
+ if (auto count = GetCountData()) {
+ count->SetName("");
+ }
+ if (auto period = GetPeriodData()) {
+ period->SetName("");
+ }
+ if (auto direction = GetDirectionData()) {
+ direction->SetName("");
+ }
+ if (auto distance = GetDistanceData()) {
+ distance->SetName("");
+ }
+ if (auto rate = GetRateData()) {
+ rate->SetName("");
+ }
+ } else {
+ if (auto distancePerPulse = GetDistancePerPulseData()) {
+ distancePerPulse->SetName(fmt::format("{} Distance/Count", name));
+ }
+ if (auto count = GetCountData()) {
+ count->SetName(fmt::format("{} Count", name));
+ }
+ if (auto period = GetPeriodData()) {
+ period->SetName(fmt::format("{} Period", name));
+ }
+ if (auto direction = GetDirectionData()) {
+ direction->SetName(fmt::format("{} Direction", name));
+ }
+ if (auto distance = GetDistanceData()) {
+ distance->SetName(fmt::format("{} Distance", name));
+ }
+ if (auto rate = GetRateData()) {
+ rate->SetName(fmt::format("{} Rate", name));
+ }
+ }
+}
+
+void glass::DisplayEncoder(EncoderModel* model) {
+ if (auto simDevice = model->GetSimDevice()) {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::TextUnformatted(simDevice);
+ ImGui::PopStyleColor();
+ return;
+ }
+
+ int chA = model->GetChannelA();
+ int chB = model->GetChannelB();
+
+ // build header label
+ std::string* name = GetStorage().GetStringRef("name");
+ char label[128];
+ if (!name->empty()) {
+ std::snprintf(label, sizeof(label), "%s [%d,%d]###name", name->c_str(), chA,
+ chB);
+ } else {
+ std::snprintf(label, sizeof(label), "Encoder[%d,%d]###name", chA, chB);
+ }
+
+ // header
+ bool open = CollapsingHeader(label);
+
+ // context menu to change name
+ if (PopupEditName("name", name)) {
+ model->SetName(name->c_str());
+ }
+
+ if (!open) {
+ return;
+ }
+
+ ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
+ // distance per pulse
+ if (auto distancePerPulseData = model->GetDistancePerPulseData()) {
+ double value = distancePerPulseData->GetValue();
+ distancePerPulseData->LabelText("Dist/Count", "%.6f", value);
+ }
+
+ // count
+ if (auto countData = model->GetCountData()) {
+ int value = countData->GetValue();
+ if (ImGui::InputInt("##input", &value)) {
+ model->SetCount(value);
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("Reset")) {
+ model->SetCount(0);
+ }
+ ImGui::SameLine();
+ ImGui::Selectable("Count");
+ countData->EmitDrag();
+ }
+
+ // max period
+ {
+ double maxPeriod = model->GetMaxPeriod();
+ ImGui::LabelText("Max Period", "%.6f", maxPeriod);
+ }
+
+ // period
+ if (auto periodData = model->GetPeriodData()) {
+ double value = periodData->GetValue();
+ if (periodData->InputDouble("Period", &value, 0, 0, "%.6g")) {
+ model->SetPeriod(value);
+ }
+ }
+
+ // reverse direction
+ ImGui::LabelText("Reverse Direction", "%s",
+ model->GetReverseDirection() ? "true" : "false");
+
+ // direction
+ if (auto directionData = model->GetDirectionData()) {
+ static const char* options[] = {"reverse", "forward"};
+ int value = directionData->GetValue() ? 1 : 0;
+ if (directionData->Combo("Direction", &value, options, 2)) {
+ model->SetDirection(value != 0);
+ }
+ }
+
+ // distance
+ if (auto distanceData = model->GetDistanceData()) {
+ double value = distanceData->GetValue();
+ if (distanceData->InputDouble("Distance", &value, 0, 0, "%.6g")) {
+ model->SetDistance(value);
+ }
+ }
+
+ // rate
+ if (auto rateData = model->GetRateData()) {
+ double value = rateData->GetValue();
+ if (rateData->InputDouble("Rate", &value, 0, 0, "%.6g")) {
+ model->SetRate(value);
+ }
+ }
+ ImGui::PopItemWidth();
+}
+
+void glass::DisplayEncoders(EncodersModel* model, std::string_view noneMsg) {
+ bool hasAny = false;
+ model->ForEachEncoder([&](EncoderModel& encoder, int i) {
+ hasAny = true;
+ PushID(i);
+ DisplayEncoder(&encoder);
+ PopID();
+ });
+ if (!hasAny && !noneMsg.empty()) {
+ ImGui::TextUnformatted(noneMsg.data(), noneMsg.data() + noneMsg.size());
+ }
+}
diff --git a/glass/src/lib/native/cpp/hardware/Gyro.cpp b/glass/src/lib/native/cpp/hardware/Gyro.cpp
new file mode 100644
index 0000000..36a3525
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/Gyro.cpp
@@ -0,0 +1,82 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/Gyro.h"
+
+#include <cmath>
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+
+#include <imgui.h>
+#include <imgui_internal.h>
+#include <wpi/numbers>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void glass::DisplayGyro(GyroModel* m) {
+ ImColor primaryColor = ImGui::GetStyle().Colors[ImGuiCol_Text];
+ ImColor disabledColor = ImGui::GetStyle().Colors[ImGuiCol_TextDisabled];
+ ImColor secondaryColor = ImGui::GetStyle().Colors[ImGuiCol_Header];
+
+ auto angle = m->GetAngleData();
+ if (!angle || !m->Exists()) {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::Text("Unknown Gyro");
+ ImGui::PopStyleColor();
+ return;
+ }
+
+ // Display the numeric angle value. This can be editable in some cases (i.e.
+ // running from HALSIM).
+ auto flags =
+ m->IsReadOnly() ? ImGuiInputTextFlags_ReadOnly : ImGuiInputTextFlags_None;
+ auto value = angle->GetValue();
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
+ if (ImGui::InputDouble("Gyro Angle (Deg)", &value, 0.0, 0.0, "%.4f", flags)) {
+ m->SetAngle(value);
+ }
+
+ // Draw the gyro indicator.
+ ImDrawList* draw = ImGui::GetWindowDrawList();
+ ImVec2 window = ImGui::GetWindowPos();
+ float w = ImGui::GetWindowWidth();
+ float h = ImGui::GetWindowHeight();
+
+ float radius = (w < h) ? w * 0.3 : h * 0.3;
+ ImVec2 center = window + ImVec2(w / 2, h / 2 + ImGui::GetFontSize());
+
+ // Add the primary circle.
+ draw->AddCircle(center, radius, primaryColor, 100, 1.5);
+
+ // Draw the spokes at every 5 degrees and a "major" spoke every 45 degrees.
+ for (int i = -175; i <= 180; i += 5) {
+ double radians = i * 2 * wpi::numbers::pi / 360.0;
+ ImVec2 direction(std::sin(radians), -std::cos(radians));
+
+ bool major = i % 45 == 0;
+ auto color = major ? primaryColor : disabledColor;
+
+ draw->AddLine(center + (direction * radius),
+ center + (direction * radius * (major ? 1.07f : 1.03f)),
+ color, 1.2f);
+ if (major) {
+ char txt[16];
+ std::snprintf(txt, sizeof(txt), "%d°", i);
+ draw->AddText(
+ center + (direction * radius * 1.25) - ImGui::CalcTextSize(txt) * 0.5,
+ primaryColor, txt, nullptr);
+ }
+ }
+
+ draw->AddCircleFilled(center, radius * 0.075, secondaryColor, 50);
+
+ double radians = value * 2 * wpi::numbers::pi / 360.0;
+ draw->AddLine(
+ center - ImVec2(1, 0),
+ center + ImVec2(std::sin(radians), -std::cos(radians)) * radius * 0.95f,
+ secondaryColor, 3);
+}
diff --git a/glass/src/lib/native/cpp/hardware/LEDDisplay.cpp b/glass/src/lib/native/cpp/hardware/LEDDisplay.cpp
new file mode 100644
index 0000000..c1ece3a
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/LEDDisplay.cpp
@@ -0,0 +1,100 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/LEDDisplay.h"
+
+#include <wpi/SmallVector.h>
+
+#include "glass/Context.h"
+#include "glass/support/ExtraGuiWidgets.h"
+
+using namespace glass;
+
+namespace {
+struct IndicatorData {
+ std::vector<int> values;
+ std::vector<ImU32> colors;
+};
+} // namespace
+
+void glass::DisplayLEDDisplay(LEDDisplayModel* model, int index) {
+ wpi::SmallVector<LEDDisplayModel::Data, 64> dataBuf;
+ auto data = model->GetData(dataBuf);
+ int length = data.size();
+ bool running = model->IsRunning();
+ auto& storage = GetStorage();
+
+ int* numColumns = storage.GetIntRef("columns", 10);
+ bool* serpentine = storage.GetBoolRef("serpentine", false);
+ int* order = storage.GetIntRef("order", LEDConfig::RowMajor);
+ int* start = storage.GetIntRef("start", LEDConfig::UpperLeft);
+
+ ImGui::PushItemWidth(ImGui::GetFontSize() * 6);
+ ImGui::LabelText("Length", "%d", length);
+ ImGui::LabelText("Running", "%s", running ? "Yes" : "No");
+ ImGui::InputInt("Columns", numColumns);
+ {
+ static const char* options[] = {"Row Major", "Column Major"};
+ ImGui::Combo("Order", order, options, 2);
+ }
+ {
+ static const char* options[] = {"Upper Left", "Lower Left", "Upper Right",
+ "Lower Right"};
+ ImGui::Combo("Start", start, options, 4);
+ }
+ ImGui::Checkbox("Serpentine", serpentine);
+ if (*numColumns < 1) {
+ *numColumns = 1;
+ }
+ ImGui::PopItemWidth();
+
+ // show as LED indicators
+ auto iData = storage.GetData<IndicatorData>();
+ if (!iData) {
+ storage.SetData(std::make_shared<IndicatorData>());
+ iData = storage.GetData<IndicatorData>();
+ }
+ if (length > static_cast<int>(iData->values.size())) {
+ iData->values.resize(length);
+ }
+ if (length > static_cast<int>(iData->colors.size())) {
+ iData->colors.resize(length);
+ }
+ if (!running) {
+ iData->colors[0] = IM_COL32(128, 128, 128, 255);
+ for (int j = 0; j < length; ++j) {
+ iData->values[j] = -1;
+ }
+ } else {
+ for (int j = 0; j < length; ++j) {
+ iData->values[j] = j + 1;
+ iData->colors[j] = IM_COL32(data[j].r, data[j].g, data[j].b, 255);
+ }
+ }
+
+ LEDConfig config;
+ config.serpentine = *serpentine;
+ config.order = static_cast<LEDConfig::Order>(*order);
+ config.start = static_cast<LEDConfig::Start>(*start);
+
+ DrawLEDs(iData->values.data(), length, *numColumns, iData->colors.data(), 0,
+ 0, config);
+}
+
+void glass::DisplayLEDDisplays(LEDDisplaysModel* model) {
+ bool hasAny = false;
+
+ model->ForEachLEDDisplay([&](LEDDisplayModel& display, int i) {
+ hasAny = true;
+ if (model->GetNumLEDDisplays() > 1) {
+ ImGui::Text("LEDs[%d]", i);
+ }
+ PushID(i);
+ DisplayLEDDisplay(&display, i);
+ PopID();
+ });
+ if (!hasAny) {
+ ImGui::Text("No addressable LEDs");
+ }
+}
diff --git a/glass/src/lib/native/cpp/hardware/PCM.cpp b/glass/src/lib/native/cpp/hardware/PCM.cpp
new file mode 100644
index 0000000..23746be
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/PCM.cpp
@@ -0,0 +1,158 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/PCM.h"
+
+#include <cstdio>
+#include <cstring>
+
+#include <imgui.h>
+#include <wpi/SmallVector.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+#include "glass/other/DeviceTree.h"
+#include "glass/support/ExtraGuiWidgets.h"
+#include "glass/support/IniSaverInfo.h"
+
+using namespace glass;
+
+bool glass::DisplayPCMSolenoids(PCMModel* model, int index,
+ bool outputsEnabled) {
+ wpi::SmallVector<int, 16> channels;
+ model->ForEachSolenoid([&](SolenoidModel& solenoid, int j) {
+ if (auto data = solenoid.GetOutputData()) {
+ if (j >= static_cast<int>(channels.size())) {
+ channels.resize(j + 1);
+ }
+ channels[j] = (outputsEnabled && data->GetValue()) ? 1 : -1;
+ }
+ });
+
+ if (channels.empty()) {
+ return false;
+ }
+
+ // show nonexistent channels as empty
+ for (auto&& ch : channels) {
+ if (ch == 0) {
+ ch = -2;
+ }
+ }
+
+ // build header label
+ std::string* name = GetStorage().GetStringRef("name");
+ char label[128];
+ if (!name->empty()) {
+ std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), index);
+ } else {
+ std::snprintf(label, sizeof(label), "PCM[%d]###name", index);
+ }
+
+ // header
+ bool open = CollapsingHeader(label);
+
+ PopupEditName("name", name);
+
+ ImGui::SetItemAllowOverlap();
+ ImGui::SameLine();
+
+ // show channels as LED indicators
+ static const ImU32 colors[] = {IM_COL32(255, 255, 102, 255),
+ IM_COL32(128, 128, 128, 255)};
+ DrawLEDs(channels.data(), channels.size(), channels.size(), colors);
+
+ if (open) {
+ ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
+ model->ForEachSolenoid([&](SolenoidModel& solenoid, int j) {
+ if (auto data = solenoid.GetOutputData()) {
+ PushID(j);
+ char solenoidName[64];
+ auto& info = data->GetNameInfo();
+ info.GetLabel(solenoidName, sizeof(solenoidName), "Solenoid", j);
+ data->LabelText(solenoidName, "%s", channels[j] == 1 ? "On" : "Off");
+ info.PopupEditName(j);
+ PopID();
+ }
+ });
+ ImGui::PopItemWidth();
+ }
+
+ return true;
+}
+
+void glass::DisplayPCMsSolenoids(PCMsModel* model, bool outputsEnabled,
+ std::string_view noneMsg) {
+ bool hasAny = false;
+ model->ForEachPCM([&](PCMModel& pcm, int i) {
+ PushID(i);
+ if (DisplayPCMSolenoids(&pcm, i, outputsEnabled)) {
+ hasAny = true;
+ }
+ PopID();
+ });
+ if (!hasAny && !noneMsg.empty()) {
+ ImGui::TextUnformatted(noneMsg.data(), noneMsg.data() + noneMsg.size());
+ }
+}
+
+void glass::DisplayCompressorDevice(PCMModel* model, int index,
+ bool outputsEnabled) {
+ auto compressor = model->GetCompressor();
+ if (!compressor || !compressor->Exists()) {
+ return;
+ }
+ DisplayCompressorDevice(compressor, index, outputsEnabled);
+}
+
+void glass::DisplayCompressorDevice(CompressorModel* model, int index,
+ bool outputsEnabled) {
+ char name[32];
+ std::snprintf(name, sizeof(name), "Compressor[%d]", index);
+ if (BeginDevice(name)) {
+ // output enabled
+ if (auto runningData = model->GetRunningData()) {
+ bool value = outputsEnabled && runningData->GetValue();
+ if (DeviceBoolean("Running", false, &value, runningData)) {
+ model->SetRunning(value);
+ }
+ }
+
+ // closed loop enabled
+ if (auto enabledData = model->GetEnabledData()) {
+ int value = enabledData->GetValue() ? 1 : 0;
+ static const char* enabledOptions[] = {"disabled", "enabled"};
+ if (DeviceEnum("Closed Loop", true, &value, enabledOptions, 2,
+ enabledData)) {
+ model->SetEnabled(value != 0);
+ }
+ }
+
+ // pressure switch
+ if (auto pressureSwitchData = model->GetPressureSwitchData()) {
+ int value = pressureSwitchData->GetValue() ? 1 : 0;
+ static const char* switchOptions[] = {"full", "low"};
+ if (DeviceEnum("Pressure", false, &value, switchOptions, 2,
+ pressureSwitchData)) {
+ model->SetPressureSwitch(value != 0);
+ }
+ }
+
+ // compressor current
+ if (auto currentData = model->GetCurrentData()) {
+ double value = currentData->GetValue();
+ if (DeviceDouble("Current (A)", false, &value, currentData)) {
+ model->SetCurrent(value);
+ }
+ }
+
+ EndDevice();
+ }
+}
+
+void glass::DisplayCompressorsDevice(PCMsModel* model, bool outputsEnabled) {
+ model->ForEachPCM([&](PCMModel& pcm, int i) {
+ DisplayCompressorDevice(&pcm, i, outputsEnabled);
+ });
+}
diff --git a/glass/src/lib/native/cpp/hardware/PWM.cpp b/glass/src/lib/native/cpp/hardware/PWM.cpp
new file mode 100644
index 0000000..3ff8e52
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/PWM.cpp
@@ -0,0 +1,63 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/PWM.h"
+
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void glass::DisplayPWM(PWMModel* model, int index, bool outputsEnabled) {
+ auto data = model->GetSpeedData();
+ if (!data) {
+ return;
+ }
+
+ // build label
+ std::string* name = GetStorage().GetStringRef("name");
+ char label[128];
+ if (!name->empty()) {
+ std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), index);
+ } else {
+ std::snprintf(label, sizeof(label), "PWM[%d]###name", index);
+ }
+
+ int led = model->GetAddressableLED();
+
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
+ if (led >= 0) {
+ ImGui::LabelText(label, "LED[%d]", led);
+ } else {
+ float val = outputsEnabled ? data->GetValue() : 0;
+ data->LabelText(label, "%0.3f", val);
+ }
+ if (PopupEditName("name", name)) {
+ data->SetName(name->c_str());
+ }
+}
+
+void glass::DisplayPWMs(PWMsModel* model, bool outputsEnabled,
+ std::string_view noneMsg) {
+ bool hasAny = false;
+ bool first = true;
+ model->ForEachPWM([&](PWMModel& pwm, int i) {
+ hasAny = true;
+ PushID(i);
+
+ if (!first) {
+ ImGui::Separator();
+ } else {
+ first = false;
+ }
+
+ DisplayPWM(&pwm, i, outputsEnabled);
+ PopID();
+ });
+ if (!hasAny && !noneMsg.empty()) {
+ ImGui::TextUnformatted(noneMsg.data(), noneMsg.data() + noneMsg.size());
+ }
+}
diff --git a/glass/src/lib/native/cpp/hardware/PowerDistribution.cpp b/glass/src/lib/native/cpp/hardware/PowerDistribution.cpp
new file mode 100644
index 0000000..46e550b
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/PowerDistribution.cpp
@@ -0,0 +1,94 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/PowerDistribution.h"
+
+#include <algorithm>
+#include <cstdio>
+
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+#include "glass/support/IniSaverInfo.h"
+
+using namespace glass;
+
+static float DisplayChannel(PowerDistributionModel& pdp, int channel) {
+ float width = 0;
+ if (auto currentData = pdp.GetCurrentData(channel)) {
+ ImGui::PushID(channel);
+ auto& leftInfo = currentData->GetNameInfo();
+ char name[64];
+ leftInfo.GetLabel(name, sizeof(name), "", channel);
+ double val = currentData->GetValue();
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
+ if (currentData->InputDouble(name, &val, 0, 0, "%.3f")) {
+ pdp.SetCurrent(channel, val);
+ }
+ width = ImGui::GetItemRectSize().x;
+ leftInfo.PopupEditName(channel);
+ ImGui::PopID();
+ }
+ return width;
+}
+
+void glass::DisplayPowerDistribution(PowerDistributionModel* model, int index) {
+ char name[128];
+ std::snprintf(name, sizeof(name), "PowerDistribution[%d]", index);
+ if (CollapsingHeader(name)) {
+ // temperature
+ if (auto tempData = model->GetTemperatureData()) {
+ double value = tempData->GetValue();
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
+ if (tempData->InputDouble("Temp", &value, 0, 0, "%.3f")) {
+ model->SetTemperature(value);
+ }
+ }
+
+ // voltage
+ if (auto voltageData = model->GetVoltageData()) {
+ double value = voltageData->GetValue();
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
+ if (voltageData->InputDouble("Voltage", &value, 0, 0, "%.3f")) {
+ model->SetVoltage(value);
+ }
+ }
+
+ // channel currents; show as two columns laid out like PowerDistribution
+ const int numChannels = model->GetNumChannels();
+ ImGui::Text("Channel Current (A)");
+ ImGui::Columns(2, "channels", false);
+ float maxWidth = ImGui::GetFontSize() * 13;
+ for (int left = 0, right = numChannels - 1; left < right; ++left, --right) {
+ float leftWidth = DisplayChannel(*model, left);
+ ImGui::NextColumn();
+
+ float rightWidth = DisplayChannel(*model, right);
+ ImGui::NextColumn();
+
+ float width =
+ (std::max)(leftWidth, rightWidth) * 2 + ImGui::GetFontSize() * 4;
+ if (width > maxWidth) {
+ maxWidth = width;
+ }
+ }
+ ImGui::Columns(1);
+ ImGui::Dummy(ImVec2(maxWidth, 0));
+ }
+}
+
+void glass::DisplayPowerDistributions(PowerDistributionsModel* model,
+ std::string_view noneMsg) {
+ bool hasAny = false;
+ model->ForEachPowerDistribution([&](PowerDistributionModel& pdp, int i) {
+ hasAny = true;
+ PushID(i);
+ DisplayPowerDistribution(&pdp, i);
+ PopID();
+ });
+ if (!hasAny && !noneMsg.empty()) {
+ ImGui::TextUnformatted(noneMsg.data(), noneMsg.data() + noneMsg.size());
+ }
+}
diff --git a/glass/src/lib/native/cpp/hardware/Relay.cpp b/glass/src/lib/native/cpp/hardware/Relay.cpp
new file mode 100644
index 0000000..59bbc51
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/Relay.cpp
@@ -0,0 +1,82 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/Relay.h"
+
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+#include "glass/support/ExtraGuiWidgets.h"
+
+using namespace glass;
+
+void glass::DisplayRelay(RelayModel* model, int index, bool outputsEnabled) {
+ auto forwardData = model->GetForwardData();
+ auto reverseData = model->GetReverseData();
+
+ if (!forwardData && !reverseData) {
+ return;
+ }
+
+ bool forward = false;
+ bool reverse = false;
+ if (outputsEnabled) {
+ if (forwardData) {
+ forward = forwardData->GetValue();
+ }
+ if (reverseData) {
+ reverse = reverseData->GetValue();
+ }
+ }
+
+ std::string* name = GetStorage().GetStringRef("name");
+ ImGui::PushID("name");
+ if (!name->empty()) {
+ ImGui::Text("%s [%d]", name->c_str(), index);
+ } else {
+ ImGui::Text("Relay[%d]", index);
+ }
+ ImGui::PopID();
+ if (PopupEditName("name", name)) {
+ if (forwardData) {
+ forwardData->SetName(name->c_str());
+ }
+ if (reverseData) {
+ reverseData->SetName(name->c_str());
+ }
+ }
+ ImGui::SameLine();
+
+ // show forward and reverse as LED indicators
+ static const ImU32 colors[] = {IM_COL32(255, 255, 102, 255),
+ IM_COL32(255, 0, 0, 255),
+ IM_COL32(128, 128, 128, 255)};
+ int values[2] = {reverseData ? (reverse ? 2 : -2) : -3,
+ forwardData ? (forward ? 1 : -1) : -3};
+ DataSource* sources[2] = {reverseData, forwardData};
+ DrawLEDSources(values, sources, 2, 2, colors);
+}
+
+void glass::DisplayRelays(RelaysModel* model, bool outputsEnabled,
+ std::string_view noneMsg) {
+ bool hasAny = false;
+ bool first = true;
+ model->ForEachRelay([&](RelayModel& relay, int i) {
+ hasAny = true;
+
+ if (!first) {
+ ImGui::Separator();
+ } else {
+ first = false;
+ }
+
+ PushID(i);
+ DisplayRelay(&relay, i, outputsEnabled);
+ PopID();
+ });
+ if (!hasAny && !noneMsg.empty()) {
+ ImGui::TextUnformatted(noneMsg.data(), noneMsg.data() + noneMsg.size());
+ }
+}
diff --git a/glass/src/lib/native/cpp/hardware/RoboRio.cpp b/glass/src/lib/native/cpp/hardware/RoboRio.cpp
new file mode 100644
index 0000000..d0667d9
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/RoboRio.cpp
@@ -0,0 +1,84 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/RoboRio.h"
+
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+static void DisplayRail(RoboRioRailModel& rail, const char* name) {
+ if (CollapsingHeader(name)) {
+ ImGui::PushID(name);
+ if (auto data = rail.GetVoltageData()) {
+ double val = data->GetValue();
+ if (data->InputDouble("Voltage (V)", &val)) {
+ rail.SetVoltage(val);
+ }
+ }
+
+ if (auto data = rail.GetCurrentData()) {
+ double val = data->GetValue();
+ if (data->InputDouble("Current (A)", &val)) {
+ rail.SetCurrent(val);
+ }
+ }
+
+ if (auto data = rail.GetActiveData()) {
+ static const char* options[] = {"inactive", "active"};
+ int val = data->GetValue() ? 1 : 0;
+ if (data->Combo("Active", &val, options, 2)) {
+ rail.SetActive(val);
+ }
+ }
+
+ if (auto data = rail.GetFaultsData()) {
+ int val = data->GetValue();
+ if (data->InputInt("Faults", &val)) {
+ rail.SetFaults(val);
+ }
+ }
+ ImGui::PopID();
+ }
+}
+
+void glass::DisplayRoboRio(RoboRioModel* model) {
+ ImGui::Button("User Button");
+ model->SetUserButton(ImGui::IsItemActive());
+
+ ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
+
+ if (CollapsingHeader("RoboRIO Input")) {
+ ImGui::PushID("RoboRIO Input");
+ if (auto data = model->GetVInVoltageData()) {
+ double val = data->GetValue();
+ if (data->InputDouble("Voltage (V)", &val)) {
+ model->SetVInVoltage(val);
+ }
+ }
+
+ if (auto data = model->GetVInCurrentData()) {
+ double val = data->GetValue();
+ if (data->InputDouble("Current (A)", &val)) {
+ model->SetVInCurrent(val);
+ }
+ }
+ ImGui::PopID();
+ }
+
+ if (auto rail = model->GetUser6VRail()) {
+ DisplayRail(*rail, "6V Rail");
+ }
+ if (auto rail = model->GetUser5VRail()) {
+ DisplayRail(*rail, "5V Rail");
+ }
+ if (auto rail = model->GetUser3V3Rail()) {
+ DisplayRail(*rail, "3.3V Rail");
+ }
+
+ ImGui::PopItemWidth();
+}
diff --git a/glass/src/lib/native/cpp/hardware/SpeedController.cpp b/glass/src/lib/native/cpp/hardware/SpeedController.cpp
new file mode 100644
index 0000000..b278401
--- /dev/null
+++ b/glass/src/lib/native/cpp/hardware/SpeedController.cpp
@@ -0,0 +1,50 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/hardware/SpeedController.h"
+
+#include <imgui.h>
+#include <imgui_internal.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void glass::DisplaySpeedController(SpeedControllerModel* m) {
+ // Get duty cycle data from the model and do not display anything if the data
+ // is null.
+ auto dc = m->GetPercentData();
+ if (!dc || !m->Exists()) {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::Text("Unknown SpeedController");
+ ImGui::PopStyleColor();
+ return;
+ }
+
+ // Set the buttons and sliders to read-only if the model is read-only.
+ if (m->IsReadOnly()) {
+ ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(210, 210, 210, 255));
+ }
+
+ // Add button to zero output.
+ if (ImGui::Button("Zero")) {
+ m->SetPercent(0.0);
+ }
+ ImGui::SameLine();
+
+ // Display a slider for the data.
+ float value = dc->GetValue();
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
+
+ if (dc->SliderFloat("% Output", &value, -1.0f, 1.0f)) {
+ m->SetPercent(value);
+ }
+
+ if (m->IsReadOnly()) {
+ ImGui::PopStyleColor();
+ ImGui::PopItemFlag();
+ }
+}
diff --git a/glass/src/lib/native/cpp/other/CommandScheduler.cpp b/glass/src/lib/native/cpp/other/CommandScheduler.cpp
new file mode 100644
index 0000000..83e4118
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/CommandScheduler.cpp
@@ -0,0 +1,40 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/CommandScheduler.h"
+
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void glass::DisplayCommandScheduler(CommandSchedulerModel* m) {
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 20);
+ ImGui::Text("Scheduled Commands: ");
+ ImGui::Separator();
+ ImGui::Spacing();
+
+ if (m->Exists()) {
+ float pos = ImGui::GetContentRegionAvail().x * 0.97f -
+ ImGui::CalcTextSize("Cancel").x;
+
+ const auto& commands = m->GetCurrentCommands();
+ for (size_t i = 0; i < commands.size(); ++i) {
+ ImGui::Text("%s", commands[i].c_str());
+ ImGui::SameLine(pos);
+
+ ImGui::PushID(i);
+ if (ImGui::Button("Cancel")) {
+ m->CancelCommand(i);
+ }
+ ImGui::PopID();
+ }
+ } else {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::Text("Unknown Scheduler");
+ ImGui::PopStyleColor();
+ }
+}
diff --git a/glass/src/lib/native/cpp/other/CommandSelector.cpp b/glass/src/lib/native/cpp/other/CommandSelector.cpp
new file mode 100644
index 0000000..d2124c3
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/CommandSelector.cpp
@@ -0,0 +1,35 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/CommandSelector.h"
+
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void glass::DisplayCommandSelector(CommandSelectorModel* m) {
+ if (auto name = m->GetName()) {
+ ImGui::Text("%s", name);
+ }
+ if (m->Exists()) {
+ if (auto run = m->GetRunningData()) {
+ bool running = run->GetValue();
+ if (ImGui::Button(running ? "Cancel" : "Run")) {
+ running = !running;
+ m->SetRunning(running);
+ }
+ ImGui::SameLine();
+ if (running) {
+ ImGui::Text("Running...");
+ }
+ }
+ } else {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::Text("Unknown Command");
+ ImGui::PopStyleColor();
+ }
+}
diff --git a/glass/src/lib/native/cpp/other/DeviceTree.cpp b/glass/src/lib/native/cpp/other/DeviceTree.cpp
new file mode 100644
index 0000000..cd69eb7
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/DeviceTree.cpp
@@ -0,0 +1,173 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/DeviceTree.h"
+
+#include <cinttypes>
+
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/ContextInternal.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void DeviceTreeModel::Update() {
+ for (auto&& display : m_displays) {
+ if (display.first) {
+ display.first->Update();
+ }
+ }
+}
+
+bool DeviceTreeModel::Exists() {
+ for (auto&& display : m_displays) {
+ if (display.first && display.first->Exists()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void DeviceTreeModel::Display() {
+ for (auto&& display : m_displays) {
+ if (display.second) {
+ display.second(display.first);
+ }
+ }
+}
+
+void glass::HideDevice(const char* id) {
+ gContext->deviceHidden[id] = true;
+}
+
+bool glass::BeginDevice(const char* id, ImGuiTreeNodeFlags flags) {
+ if (gContext->deviceHidden[id]) {
+ return false;
+ }
+
+ PushID(id);
+
+ // build label
+ std::string* name = GetStorage().GetStringRef("name");
+ char label[128];
+ std::snprintf(label, sizeof(label), "%s###name",
+ name->empty() ? id : name->c_str());
+
+ bool open = CollapsingHeader(label, flags);
+ PopupEditName("name", name);
+
+ if (!open) {
+ PopID();
+ }
+ return open;
+}
+
+void glass::EndDevice() {
+ PopID();
+}
+
+static bool DeviceBooleanImpl(const char* name, bool readonly, bool* value) {
+ if (readonly) {
+ ImGui::LabelText(name, "%s", *value ? "true" : "false");
+ } else {
+ static const char* boolOptions[] = {"false", "true"};
+ int val = *value ? 1 : 0;
+ if (ImGui::Combo(name, &val, boolOptions, 2)) {
+ *value = val;
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool DeviceDoubleImpl(const char* name, bool readonly, double* value) {
+ if (readonly) {
+ ImGui::LabelText(name, "%.6f", *value);
+ return false;
+ } else {
+ return ImGui::InputDouble(name, value, 0, 0, "%.6f",
+ ImGuiInputTextFlags_EnterReturnsTrue);
+ }
+}
+
+static bool DeviceEnumImpl(const char* name, bool readonly, int* value,
+ const char** options, int32_t numOptions) {
+ if (readonly) {
+ if (*value < 0 || *value >= numOptions) {
+ ImGui::LabelText(name, "%d (unknown)", *value);
+ } else {
+ ImGui::LabelText(name, "%s", options[*value]);
+ }
+ return false;
+ } else {
+ return ImGui::Combo(name, value, options, numOptions);
+ }
+}
+
+static bool DeviceIntImpl(const char* name, bool readonly, int32_t* value) {
+ if (readonly) {
+ ImGui::LabelText(name, "%" PRId32, *value);
+ return false;
+ } else {
+ return ImGui::InputScalar(name, ImGuiDataType_S32, value, nullptr, nullptr,
+ nullptr, ImGuiInputTextFlags_EnterReturnsTrue);
+ }
+}
+
+static bool DeviceLongImpl(const char* name, bool readonly, int64_t* value) {
+ if (readonly) {
+ ImGui::LabelText(name, "%" PRId64, *value);
+ return false;
+ } else {
+ return ImGui::InputScalar(name, ImGuiDataType_S64, value, nullptr, nullptr,
+ nullptr, ImGuiInputTextFlags_EnterReturnsTrue);
+ }
+}
+
+template <typename F, typename... Args>
+static inline bool DeviceValueImpl(const char* name, bool readonly,
+ const DataSource* source, F&& func,
+ Args... args) {
+ ImGui::SetNextItemWidth(ImGui::GetWindowWidth() * 0.5f);
+ if (!source) {
+ return func(name, readonly, args...);
+ } else {
+ ImGui::PushID(name);
+ bool rv = func("", readonly, args...);
+ ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
+ ImGui::Selectable(name);
+ source->EmitDrag();
+ ImGui::PopID();
+ return rv;
+ }
+}
+
+bool glass::DeviceBoolean(const char* name, bool readonly, bool* value,
+ const DataSource* source) {
+ return DeviceValueImpl(name, readonly, source, DeviceBooleanImpl, value);
+}
+
+bool glass::DeviceDouble(const char* name, bool readonly, double* value,
+ const DataSource* source) {
+ return DeviceValueImpl(name, readonly, source, DeviceDoubleImpl, value);
+}
+
+bool glass::DeviceEnum(const char* name, bool readonly, int* value,
+ const char** options, int32_t numOptions,
+ const DataSource* source) {
+ return DeviceValueImpl(name, readonly, source, DeviceEnumImpl, value, options,
+ numOptions);
+}
+
+bool glass::DeviceInt(const char* name, bool readonly, int32_t* value,
+ const DataSource* source) {
+ return DeviceValueImpl(name, readonly, source, DeviceIntImpl, value);
+}
+
+bool glass::DeviceLong(const char* name, bool readonly, int64_t* value,
+ const DataSource* source) {
+ return DeviceValueImpl(name, readonly, source, DeviceLongImpl, value);
+}
diff --git a/glass/src/lib/native/cpp/other/Drive.cpp b/glass/src/lib/native/cpp/other/Drive.cpp
new file mode 100644
index 0000000..9dc1675
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/Drive.cpp
@@ -0,0 +1,139 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/Drive.h"
+
+#include <array>
+#include <cmath>
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+
+#include <imgui.h>
+#include <imgui_internal.h>
+#include <wpi/numbers>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void glass::DisplayDrive(DriveModel* m) {
+ // Check if the model exists.
+ if (!m->Exists()) {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::Text("Unknown Drive");
+ ImGui::PopStyleColor();
+ return;
+ }
+
+ const auto& wheels = m->GetWheels();
+ ImDrawList* draw = ImGui::GetWindowDrawList();
+ ImColor color = ImGui::GetStyle().Colors[ImGuiCol_Text];
+
+ // Get window position and size.
+ ImVec2 pos = ImGui::GetWindowPos();
+ ImVec2 size = ImGui::GetWindowSize();
+
+ // Calculate corners for drivetrain body.
+ float x1 = pos.x + 60.0f;
+ float y1 = pos.y + ImGui::GetFontSize() * 2.0f;
+ float x2 = pos.x + size.x - 60.0f;
+ float y2 = pos.y + size.y - ImGui::GetFontSize() * 2.0f * wheels.size();
+
+ // Draw the primary rectangle.
+ draw->AddRect(ImVec2(x1, y1), ImVec2(x2, y2), color);
+
+ // Display the speed vector.
+ ImVec2 center{(x1 + x2) / 2.0f, (y1 + y2) / 2.0f};
+ ImVec2 speed = m->GetSpeedVector();
+ ImVec2 arrow = center + speed * 50.0f;
+
+ draw->AddLine(center, arrow, color, 2.0f);
+
+ auto drawArrow = [draw, &color](const ImVec2& arrowPos, float angle) {
+ draw->AddTriangleFilled(
+ arrowPos,
+ arrowPos + ImRotate(ImVec2(0.0f, 7.5f),
+ std::cos(angle + wpi::numbers::pi / 4),
+ std::sin(angle + wpi::numbers::pi / 4)),
+ arrowPos + ImRotate(ImVec2(0.0f, 7.5f),
+ std::cos(angle - wpi::numbers::pi / 4),
+ std::sin(angle - wpi::numbers::pi / 4)),
+ color);
+ };
+
+ // Draw the arrow if there is any translation; draw an X otherwise.
+ if (std::abs(speed.y) > 0 || std::abs(speed.x) > 0) {
+ drawArrow(arrow, std::atan2(speed.x, -speed.y));
+ } else {
+ ImVec2 a{7.5f, +7.5f};
+ ImVec2 b{7.5f, -7.5f};
+ draw->AddLine(center + a, center - a, color);
+ draw->AddLine(center + b, center - b, color);
+ }
+
+ // Calculate the positions of the top-left corner of the wheels.
+ std::array<ImVec2, 4> corners{
+ ImVec2(x1 - 25.0f, y1 + 10.0f), ImVec2(x1 - 25.0f, y2 - 70.0f),
+ ImVec2(x2 + 00.0f, y1 + 10.0f), ImVec2(x2 + 00.0f, y2 - 70.0f)};
+
+ // Draw the wheels.
+ for (auto&& corner : corners) {
+ draw->AddRect(corner, corner + ImVec2(25.0f, 60.0f), color);
+ }
+
+ // Show rotation
+ double rotation = m->GetRotation();
+ if (rotation != 0) {
+ float radius = 60.0f;
+ double a1 = 0.0;
+ double a2 = wpi::numbers::pi / 2 * rotation;
+
+ draw->PathArcTo(center, radius, a1, a2, 20);
+ draw->PathStroke(color, false);
+ draw->PathArcTo(center, radius, a1 + wpi::numbers::pi,
+ a2 + wpi::numbers::pi, 20);
+ draw->PathStroke(color, false);
+
+ double adder = rotation < 0 ? wpi::numbers::pi : 0;
+
+ auto arrowPos =
+ center + ImVec2(radius * -std::cos(a2), radius * -std::sin(a2));
+ drawArrow(arrowPos, a2 + adder);
+
+ a2 += wpi::numbers::pi;
+ arrowPos = center + ImVec2(radius * -std::cos(a2), radius * -std::sin(a2));
+ drawArrow(arrowPos, a2 + adder);
+ }
+
+ // Set the buttons and sliders to read-only if the model is read-only.
+ if (m->IsReadOnly()) {
+ ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(210, 210, 210, 255));
+ }
+
+ // Add sliders for the wheel percentages.
+ ImGui::SetCursorPosY(y2 - pos.y + ImGui::GetFontSize() * 0.5);
+ for (auto&& wheel : wheels) {
+ if (wheel.percent) {
+ ImGui::PushID(wheel.name.c_str());
+ if (ImGui::Button("Zero")) {
+ wheel.setter(0.0);
+ }
+ ImGui::PopID();
+
+ ImGui::SameLine();
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8.0f);
+ float value = wheel.percent->GetValue();
+ if (wheel.percent->SliderFloat(wheel.name.c_str(), &value, -1.0f, 1.0f)) {
+ wheel.setter(value);
+ }
+ }
+ }
+
+ if (m->IsReadOnly()) {
+ ImGui::PopStyleColor();
+ ImGui::PopItemFlag();
+ }
+}
diff --git a/glass/src/lib/native/cpp/other/FMS.cpp b/glass/src/lib/native/cpp/other/FMS.cpp
new file mode 100644
index 0000000..a19cad4
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/FMS.cpp
@@ -0,0 +1,150 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/FMS.h"
+
+#include <imgui.h>
+#include <wpi/SmallString.h>
+
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+static const char* stations[] = {"Red 1", "Red 2", "Red 3",
+ "Blue 1", "Blue 2", "Blue 3"};
+
+void glass::DisplayFMS(FMSModel* model, bool* matchTimeEnabled) {
+ if (!model->Exists() || model->IsReadOnly()) {
+ return DisplayFMSReadOnly(model);
+ }
+
+ // FMS Attached
+ if (auto data = model->GetFmsAttachedData()) {
+ bool val = data->GetValue();
+ if (ImGui::Checkbox("FMS Attached", &val)) {
+ model->SetFmsAttached(val);
+ }
+ data->EmitDrag();
+ }
+
+ // DS Attached
+ if (auto data = model->GetDsAttachedData()) {
+ bool val = data->GetValue();
+ if (ImGui::Checkbox("DS Attached", &val)) {
+ model->SetDsAttached(val);
+ }
+ data->EmitDrag();
+ }
+
+ // Alliance Station
+ if (auto data = model->GetAllianceStationIdData()) {
+ int val = data->GetValue();
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
+ if (ImGui::Combo("Alliance Station", &val, stations, 6)) {
+ model->SetAllianceStationId(val);
+ }
+ data->EmitDrag();
+ }
+
+ // Match Time
+ if (auto data = model->GetMatchTimeData()) {
+ if (matchTimeEnabled) {
+ ImGui::Checkbox("Match Time Enabled", matchTimeEnabled);
+ }
+
+ double val = data->GetValue();
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
+ if (ImGui::InputDouble("Match Time", &val, 0, 0, "%.1f",
+ ImGuiInputTextFlags_EnterReturnsTrue)) {
+ model->SetMatchTime(val);
+ }
+ data->EmitDrag();
+ ImGui::SameLine();
+ if (ImGui::Button("Reset")) {
+ model->SetMatchTime(0.0);
+ }
+ }
+
+ // Game Specific Message
+ // make buffer full 64 width, null terminated, for editability
+ wpi::SmallString<64> gameSpecificMessage;
+ model->GetGameSpecificMessage(gameSpecificMessage);
+ gameSpecificMessage.resize(63);
+ gameSpecificMessage.push_back('\0');
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
+ if (ImGui::InputText("Game Specific", gameSpecificMessage.data(),
+ gameSpecificMessage.size(),
+ ImGuiInputTextFlags_EnterReturnsTrue)) {
+ model->SetGameSpecificMessage(gameSpecificMessage.data());
+ }
+}
+
+void glass::DisplayFMSReadOnly(FMSModel* model) {
+ bool exists = model->Exists();
+ if (!exists) {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ }
+
+ if (auto data = model->GetEStopData()) {
+ ImGui::Selectable("E-Stopped: ");
+ data->EmitDrag();
+ ImGui::SameLine();
+ ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
+ }
+ if (auto data = model->GetEnabledData()) {
+ ImGui::Selectable("Robot Enabled: ");
+ data->EmitDrag();
+ ImGui::SameLine();
+ ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
+ }
+ if (auto data = model->GetTestData()) {
+ ImGui::Selectable("Test Mode: ");
+ data->EmitDrag();
+ ImGui::SameLine();
+ ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
+ }
+ if (auto data = model->GetAutonomousData()) {
+ ImGui::Selectable("Autonomous Mode: ");
+ data->EmitDrag();
+ ImGui::SameLine();
+ ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
+ }
+ if (auto data = model->GetFmsAttachedData()) {
+ ImGui::Selectable("FMS Attached: ");
+ data->EmitDrag();
+ ImGui::SameLine();
+ ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
+ }
+ if (auto data = model->GetDsAttachedData()) {
+ ImGui::Selectable("DS Attached: ");
+ data->EmitDrag();
+ ImGui::SameLine();
+ ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
+ }
+ if (auto data = model->GetAllianceStationIdData()) {
+ ImGui::Selectable("Alliance Station: ");
+ data->EmitDrag();
+ ImGui::SameLine();
+ ImGui::TextUnformatted(exists ? stations[static_cast<int>(data->GetValue())]
+ : "?");
+ }
+ if (auto data = model->GetMatchTimeData()) {
+ ImGui::Selectable("Match Time: ");
+ data->EmitDrag();
+ ImGui::SameLine();
+ if (exists) {
+ ImGui::Text("%.1f", data->GetValue());
+ } else {
+ ImGui::TextUnformatted("?");
+ }
+ }
+
+ wpi::SmallString<64> gameSpecificMessage;
+ model->GetGameSpecificMessage(gameSpecificMessage);
+ ImGui::Text("Game Specific: %s", exists ? gameSpecificMessage.c_str() : "?");
+
+ if (!exists) {
+ ImGui::PopStyleColor();
+ }
+}
diff --git a/glass/src/lib/native/cpp/other/Field2D.cpp b/glass/src/lib/native/cpp/other/Field2D.cpp
new file mode 100644
index 0000000..ec0210e
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/Field2D.cpp
@@ -0,0 +1,1227 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/Field2D.h"
+
+#include <algorithm>
+#include <cmath>
+#include <cstdio>
+#include <memory>
+#include <string_view>
+#include <utility>
+
+#include <fmt/format.h>
+#include <frc/geometry/Pose2d.h>
+#include <frc/geometry/Rotation2d.h>
+#include <frc/geometry/Translation2d.h>
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+#include <imgui.h>
+#include <imgui_internal.h>
+#include <imgui_stdlib.h>
+#include <portable-file-dialogs.h>
+#include <units/angle.h>
+#include <units/length.h>
+#include <wpi/SmallString.h>
+#include <wpi/StringExtras.h>
+#include <wpi/StringMap.h>
+#include <wpi/fs.h>
+#include <wpi/json.h>
+#include <wpi/raw_istream.h>
+#include <wpigui.h>
+
+#include "glass/Context.h"
+
+using namespace glass;
+
+namespace gui = wpi::gui;
+
+namespace {
+
+enum DisplayUnits { kDisplayMeters = 0, kDisplayFeet, kDisplayInches };
+
+// Per-frame field data (not persistent)
+struct FieldFrameData {
+ frc::Translation2d GetPosFromScreen(const ImVec2& cursor) const {
+ return {
+ units::meter_t{(std::clamp(cursor.x, min.x, max.x) - min.x) / scale},
+ units::meter_t{(max.y - std::clamp(cursor.y, min.y, max.y)) / scale}};
+ }
+ ImVec2 GetScreenFromPos(const frc::Translation2d& pos) const {
+ return {min.x + scale * pos.X().to<float>(),
+ max.y - scale * pos.Y().to<float>()};
+ }
+
+ // in screen coordinates
+ ImVec2 imageMin;
+ ImVec2 imageMax;
+ ImVec2 min;
+ ImVec2 max;
+
+ float scale; // scaling from meters to screen units
+};
+
+// Pose drag target info
+struct SelectedTargetInfo {
+ FieldObjectModel* objModel = nullptr;
+ std::string name;
+ size_t index;
+ units::radian_t rot;
+ ImVec2 poseCenter; // center of the pose (screen coordinates)
+ ImVec2 center; // center of the target (screen coordinates)
+ float radius; // target radius
+ float dist; // distance from center to mouse
+ int corner; // corner (1 = center)
+};
+
+// Pose drag state
+struct PoseDragState {
+ SelectedTargetInfo target;
+ ImVec2 initialOffset;
+ units::radian_t initialAngle = 0_rad;
+};
+
+// Popup edit state
+class PopupState {
+ public:
+ void Open(SelectedTargetInfo* target, const frc::Translation2d& pos);
+ void Close();
+
+ SelectedTargetInfo* GetTarget() { return &m_target; }
+ FieldObjectModel* GetInsertModel() { return m_insertModel; }
+ wpi::span<const frc::Pose2d> GetInsertPoses() const { return m_insertPoses; }
+
+ void Display(Field2DModel* model, const FieldFrameData& ffd);
+
+ private:
+ void DisplayTarget(Field2DModel* model, const FieldFrameData& ffd);
+ void DisplayInsert(Field2DModel* model);
+
+ SelectedTargetInfo m_target;
+
+ // for insert
+ FieldObjectModel* m_insertModel;
+ std::vector<frc::Pose2d> m_insertPoses;
+ std::string m_insertName;
+ int m_insertIndex;
+};
+
+struct DisplayOptions {
+ explicit DisplayOptions(const gui::Texture& texture) : texture{texture} {}
+
+ enum Style { kBoxImage = 0, kLine, kLineClosed, kTrack };
+
+ static constexpr Style kDefaultStyle = kBoxImage;
+ static constexpr float kDefaultWeight = 4.0f;
+ static constexpr ImU32 kDefaultColor = IM_COL32(255, 0, 0, 255);
+ static constexpr auto kDefaultWidth = 0.6858_m;
+ static constexpr auto kDefaultLength = 0.8204_m;
+ static constexpr bool kDefaultArrows = true;
+ static constexpr int kDefaultArrowSize = 50;
+ static constexpr float kDefaultArrowWeight = 4.0f;
+ static constexpr ImU32 kDefaultArrowColor = IM_COL32(0, 255, 0, 255);
+ static constexpr bool kDefaultSelectable = true;
+
+ Style style = kDefaultStyle;
+ float weight = kDefaultWeight;
+ int color = kDefaultColor;
+
+ units::meter_t width = kDefaultWidth;
+ units::meter_t length = kDefaultLength;
+
+ bool arrows = kDefaultArrows;
+ int arrowSize = kDefaultArrowSize;
+ float arrowWeight = kDefaultArrowWeight;
+ int arrowColor = kDefaultArrowColor;
+
+ bool selectable = kDefaultSelectable;
+
+ const gui::Texture& texture;
+};
+
+// Per-frame pose data (not persistent)
+class PoseFrameData {
+ public:
+ explicit PoseFrameData(const frc::Pose2d& pose, FieldObjectModel& model,
+ size_t index, const FieldFrameData& ffd,
+ const DisplayOptions& displayOptions);
+ void SetPosition(const frc::Translation2d& pos);
+ void SetRotation(units::radian_t rot);
+ const frc::Rotation2d& GetRotation() const { return m_pose.Rotation(); }
+ const frc::Pose2d& GetPose() const { return m_pose; }
+ float GetHitRadius() const { return m_hitRadius; }
+ void UpdateFrameData();
+ std::pair<int, float> IsHovered(const ImVec2& cursor) const;
+ SelectedTargetInfo GetDragTarget(int corner, float dist) const;
+ void HandleDrag(const ImVec2& cursor);
+ void Draw(ImDrawList* drawList, std::vector<ImVec2>* center,
+ std::vector<ImVec2>* left, std::vector<ImVec2>* right) const;
+
+ // in window coordinates
+ ImVec2 m_center;
+ ImVec2 m_corners[6]; // 5 and 6 are used for track width
+ ImVec2 m_arrow[3];
+
+ private:
+ FieldObjectModel& m_model;
+ size_t m_index;
+ const FieldFrameData& m_ffd;
+ const DisplayOptions& m_displayOptions;
+
+ // scaled width/2 and length/2, in screen units
+ float m_width2;
+ float m_length2;
+
+ float m_hitRadius;
+
+ frc::Pose2d m_pose;
+};
+
+class ObjectInfo {
+ public:
+ ObjectInfo();
+
+ DisplayOptions GetDisplayOptions() const;
+ void DisplaySettings();
+ void DrawLine(ImDrawList* drawList, wpi::span<const ImVec2> points) const;
+
+ void LoadImage();
+ const gui::Texture& GetTexture() const { return m_texture; }
+
+ private:
+ void Reset();
+ bool LoadImageImpl(const char* fn);
+
+ std::unique_ptr<pfd::open_file> m_fileOpener;
+
+ // in meters
+ float* m_pWidth;
+ float* m_pLength;
+
+ int* m_pStyle; // DisplayOptions::Style
+ float* m_pWeight;
+ int* m_pColor;
+
+ bool* m_pArrows;
+ int* m_pArrowSize;
+ float* m_pArrowWeight;
+ int* m_pArrowColor;
+
+ bool* m_pSelectable;
+
+ std::string* m_pFilename;
+ gui::Texture m_texture;
+};
+
+class FieldInfo {
+ public:
+ static constexpr auto kDefaultWidth = 15.98_m;
+ static constexpr auto kDefaultHeight = 8.21_m;
+
+ FieldInfo();
+
+ void DisplaySettings();
+
+ void LoadImage();
+ FieldFrameData GetFrameData(ImVec2 min, ImVec2 max) const;
+ void Draw(ImDrawList* drawList, const FieldFrameData& frameData) const;
+
+ wpi::StringMap<std::unique_ptr<ObjectInfo>> m_objects;
+
+ private:
+ void Reset();
+ bool LoadImageImpl(const char* fn);
+ void LoadJson(std::string_view jsonfile);
+
+ std::unique_ptr<pfd::open_file> m_fileOpener;
+
+ std::string* m_pFilename;
+ gui::Texture m_texture;
+
+ // in meters
+ float* m_pWidth;
+ float* m_pHeight;
+
+ // in image pixels
+ int m_imageWidth;
+ int m_imageHeight;
+ int* m_pTop;
+ int* m_pLeft;
+ int* m_pBottom;
+ int* m_pRight;
+};
+
+} // namespace
+
+static PoseDragState gDragState;
+static PopupState gPopupState;
+static DisplayUnits gDisplayUnits = kDisplayMeters;
+
+static double ConvertDisplayLength(units::meter_t v) {
+ switch (gDisplayUnits) {
+ case kDisplayFeet:
+ return v.convert<units::feet>().value();
+ case kDisplayInches:
+ return v.convert<units::inches>().value();
+ case kDisplayMeters:
+ default:
+ return v.value();
+ }
+}
+
+static double ConvertDisplayAngle(units::degree_t v) {
+ return v.value();
+}
+
+static bool InputLength(const char* label, units::meter_t* v, double step = 0.0,
+ double step_fast = 0.0, const char* format = "%.6f",
+ ImGuiInputTextFlags flags = 0) {
+ double dv = ConvertDisplayLength(*v);
+ if (ImGui::InputDouble(label, &dv, step, step_fast, format, flags)) {
+ switch (gDisplayUnits) {
+ case kDisplayFeet:
+ *v = units::foot_t{dv};
+ break;
+ case kDisplayInches:
+ *v = units::inch_t{dv};
+ break;
+ case kDisplayMeters:
+ default:
+ *v = units::meter_t{dv};
+ break;
+ }
+ return true;
+ }
+ return false;
+}
+
+static bool InputFloatLength(const char* label, float* v, double step = 0.0,
+ double step_fast = 0.0,
+ const char* format = "%.3f",
+ ImGuiInputTextFlags flags = 0) {
+ units::meter_t uv{*v};
+ if (InputLength(label, &uv, step, step_fast, format, flags)) {
+ *v = uv.to<float>();
+ return true;
+ }
+ return false;
+}
+
+static bool InputAngle(const char* label, units::degree_t* v, double step = 0.0,
+ double step_fast = 0.0, const char* format = "%.6f",
+ ImGuiInputTextFlags flags = 0) {
+ double dv = ConvertDisplayAngle(*v);
+ if (ImGui::InputDouble(label, &dv, step, step_fast, format, flags)) {
+ *v = units::degree_t{dv};
+ return true;
+ }
+ return false;
+}
+
+static bool InputPose(frc::Pose2d* pose) {
+ auto x = pose->X();
+ auto y = pose->Y();
+ auto rot = pose->Rotation().Degrees();
+
+ bool changed;
+ changed = InputLength("x", &x);
+ changed = InputLength("y", &y) || changed;
+ changed = InputAngle("rot", &rot) || changed;
+ if (changed) {
+ *pose = frc::Pose2d{x, y, rot};
+ }
+ return changed;
+}
+
+FieldInfo::FieldInfo() {
+ auto& storage = GetStorage();
+ m_pFilename = storage.GetStringRef("image");
+ m_pTop = storage.GetIntRef("top", 0);
+ m_pLeft = storage.GetIntRef("left", 0);
+ m_pBottom = storage.GetIntRef("bottom", -1);
+ m_pRight = storage.GetIntRef("right", -1);
+ m_pWidth = storage.GetFloatRef("width", kDefaultWidth.to<float>());
+ m_pHeight = storage.GetFloatRef("height", kDefaultHeight.to<float>());
+}
+
+void FieldInfo::DisplaySettings() {
+ if (ImGui::Button("Choose image...")) {
+ m_fileOpener = std::make_unique<pfd::open_file>(
+ "Choose field image", "",
+ std::vector<std::string>{"Image File",
+ "*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif "
+ "*.hdr *.pic *.ppm *.pgm",
+ "PathWeaver JSON File", "*.json"});
+ }
+ if (ImGui::Button("Reset image")) {
+ Reset();
+ }
+ InputFloatLength("Field Width", m_pWidth);
+ InputFloatLength("Field Height", m_pHeight);
+ // ImGui::InputInt("Field Top", m_pTop);
+ // ImGui::InputInt("Field Left", m_pLeft);
+ // ImGui::InputInt("Field Right", m_pRight);
+ // ImGui::InputInt("Field Bottom", m_pBottom);
+}
+
+void FieldInfo::Reset() {
+ m_texture = gui::Texture{};
+ m_pFilename->clear();
+ m_imageWidth = 0;
+ m_imageHeight = 0;
+ *m_pTop = 0;
+ *m_pLeft = 0;
+ *m_pBottom = -1;
+ *m_pRight = -1;
+}
+
+void FieldInfo::LoadImage() {
+ if (m_fileOpener && m_fileOpener->ready(0)) {
+ auto result = m_fileOpener->result();
+ if (!result.empty()) {
+ if (wpi::ends_with(result[0], ".json")) {
+ LoadJson(result[0]);
+ } else {
+ LoadImageImpl(result[0].c_str());
+ *m_pTop = 0;
+ *m_pLeft = 0;
+ *m_pBottom = -1;
+ *m_pRight = -1;
+ }
+ }
+ m_fileOpener.reset();
+ }
+ if (!m_texture && !m_pFilename->empty()) {
+ if (!LoadImageImpl(m_pFilename->c_str())) {
+ m_pFilename->clear();
+ }
+ }
+}
+
+void FieldInfo::LoadJson(std::string_view jsonfile) {
+ std::error_code ec;
+ wpi::raw_fd_istream f(jsonfile, ec);
+ if (ec) {
+ std::fputs("GUI: could not open field JSON file\n", stderr);
+ return;
+ }
+
+ // parse file
+ wpi::json j;
+ try {
+ j = wpi::json::parse(f);
+ } catch (const wpi::json::parse_error& e) {
+ fmt::print(stderr, "GUI: JSON: could not parse: {}\n", e.what());
+ }
+
+ // top level must be an object
+ if (!j.is_object()) {
+ std::fputs("GUI: JSON: does not contain a top object\n", stderr);
+ return;
+ }
+
+ // image filename
+ std::string image;
+ try {
+ image = j.at("field-image").get<std::string>();
+ } catch (const wpi::json::exception& e) {
+ fmt::print(stderr, "GUI: JSON: could not read field-image: {}\n", e.what());
+ return;
+ }
+
+ // corners
+ int top, left, bottom, right;
+ try {
+ top = j.at("field-corners").at("top-left").at(1).get<int>();
+ left = j.at("field-corners").at("top-left").at(0).get<int>();
+ bottom = j.at("field-corners").at("bottom-right").at(1).get<int>();
+ right = j.at("field-corners").at("bottom-right").at(0).get<int>();
+ } catch (const wpi::json::exception& e) {
+ fmt::print(stderr, "GUI: JSON: could not read field-corners: {}\n",
+ e.what());
+ return;
+ }
+
+ // size
+ float width;
+ float height;
+ try {
+ width = j.at("field-size").at(0).get<float>();
+ height = j.at("field-size").at(1).get<float>();
+ } catch (const wpi::json::exception& e) {
+ fmt::print(stderr, "GUI: JSON: could not read field-size: {}\n", e.what());
+ return;
+ }
+
+ // units for size
+ std::string unit;
+ try {
+ unit = j.at("field-unit").get<std::string>();
+ } catch (const wpi::json::exception& e) {
+ fmt::print(stderr, "GUI: JSON: could not read field-unit: {}\n", e.what());
+ return;
+ }
+
+ // convert size units to meters
+ if (unit == "foot" || unit == "feet") {
+ width = units::convert<units::feet, units::meters>(width);
+ height = units::convert<units::feet, units::meters>(height);
+ }
+
+ // the image filename is relative to the json file
+ auto pathname = fs::path{jsonfile}.replace_filename(image).string();
+
+ // load field image
+ if (!LoadImageImpl(pathname.c_str())) {
+ return;
+ }
+
+ // save to field info
+ *m_pFilename = pathname;
+ *m_pTop = top;
+ *m_pLeft = left;
+ *m_pBottom = bottom;
+ *m_pRight = right;
+ *m_pWidth = width;
+ *m_pHeight = height;
+}
+
+bool FieldInfo::LoadImageImpl(const char* fn) {
+ fmt::print("GUI: loading field image '{}'\n", fn);
+ auto texture = gui::Texture::CreateFromFile(fn);
+ if (!texture) {
+ std::puts("GUI: could not read field image");
+ return false;
+ }
+ m_texture = std::move(texture);
+ m_imageWidth = m_texture.GetWidth();
+ m_imageHeight = m_texture.GetHeight();
+ *m_pFilename = fn;
+ return true;
+}
+
+FieldFrameData FieldInfo::GetFrameData(ImVec2 min, ImVec2 max) const {
+ // fit the image into the window
+ if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) {
+ gui::MaxFit(&min, &max, m_imageWidth, m_imageHeight);
+ }
+
+ FieldFrameData ffd;
+ ffd.imageMin = min;
+ ffd.imageMax = max;
+
+ // size down the box by the image corners (if any)
+ if (*m_pBottom > 0 && *m_pRight > 0) {
+ min.x += *m_pLeft * (max.x - min.x) / m_imageWidth;
+ min.y += *m_pTop * (max.y - min.y) / m_imageHeight;
+ max.x -= (m_imageWidth - *m_pRight) * (max.x - min.x) / m_imageWidth;
+ max.y -= (m_imageHeight - *m_pBottom) * (max.y - min.y) / m_imageHeight;
+ }
+
+ // draw the field "active area" as a yellow boundary box
+ gui::MaxFit(&min, &max, *m_pWidth, *m_pHeight);
+
+ ffd.min = min;
+ ffd.max = max;
+ ffd.scale = (max.x - min.x) / *m_pWidth;
+ return ffd;
+}
+
+void FieldInfo::Draw(ImDrawList* drawList, const FieldFrameData& ffd) const {
+ if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) {
+ drawList->AddImage(m_texture, ffd.imageMin, ffd.imageMax);
+ }
+
+ // draw the field "active area" as a yellow boundary box
+ drawList->AddRect(ffd.min, ffd.max, IM_COL32(255, 255, 0, 255));
+}
+
+ObjectInfo::ObjectInfo() {
+ auto& storage = GetStorage();
+ m_pFilename = storage.GetStringRef("image");
+ m_pWidth =
+ storage.GetFloatRef("width", DisplayOptions::kDefaultWidth.to<float>());
+ m_pLength =
+ storage.GetFloatRef("length", DisplayOptions::kDefaultLength.to<float>());
+ m_pStyle = storage.GetIntRef("style", DisplayOptions::kDefaultStyle);
+ m_pWeight = storage.GetFloatRef("weight", DisplayOptions::kDefaultWeight);
+ m_pColor = storage.GetIntRef("color", DisplayOptions::kDefaultColor);
+ m_pArrows = storage.GetBoolRef("arrows", DisplayOptions::kDefaultArrows);
+ m_pArrowSize =
+ storage.GetIntRef("arrowSize", DisplayOptions::kDefaultArrowSize);
+ m_pArrowWeight =
+ storage.GetFloatRef("arrowWeight", DisplayOptions::kDefaultArrowWeight);
+ m_pArrowColor =
+ storage.GetIntRef("arrowColor", DisplayOptions::kDefaultArrowColor);
+ m_pSelectable =
+ storage.GetBoolRef("selectable", DisplayOptions::kDefaultSelectable);
+}
+
+DisplayOptions ObjectInfo::GetDisplayOptions() const {
+ DisplayOptions rv{m_texture};
+ rv.style = static_cast<DisplayOptions::Style>(*m_pStyle);
+ rv.weight = *m_pWeight;
+ rv.color = *m_pColor;
+ rv.width = units::meter_t{*m_pWidth};
+ rv.length = units::meter_t{*m_pLength};
+ rv.arrows = *m_pArrows;
+ rv.arrowSize = *m_pArrowSize;
+ rv.arrowWeight = *m_pArrowWeight;
+ rv.arrowColor = *m_pArrowColor;
+ rv.selectable = *m_pSelectable;
+ return rv;
+}
+
+void ObjectInfo::DisplaySettings() {
+ static const char* styleChoices[] = {"Box/Image", "Line", "Line (Closed)",
+ "Track"};
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
+ ImGui::Combo("Style", m_pStyle, styleChoices, IM_ARRAYSIZE(styleChoices));
+ switch (*m_pStyle) {
+ case DisplayOptions::kBoxImage:
+ if (ImGui::Button("Choose image...")) {
+ m_fileOpener = std::make_unique<pfd::open_file>(
+ "Choose object image", "",
+ std::vector<std::string>{
+ "Image File",
+ "*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif "
+ "*.hdr *.pic *.ppm *.pgm"});
+ }
+ if (ImGui::Button("Reset image")) {
+ Reset();
+ }
+ InputFloatLength("Width", m_pWidth);
+ InputFloatLength("Length", m_pLength);
+ break;
+ case DisplayOptions::kTrack:
+ InputFloatLength("Width", m_pWidth);
+ break;
+ default:
+ break;
+ }
+
+ ImGui::InputFloat("Line Weight", m_pWeight);
+ ImColor col(*m_pColor);
+ if (ImGui::ColorEdit3("Line Color", &col.Value.x,
+ ImGuiColorEditFlags_NoInputs)) {
+ *m_pColor = col;
+ }
+ ImGui::Checkbox("Arrows", m_pArrows);
+ if (*m_pArrows) {
+ ImGui::SliderInt("Arrow Size", m_pArrowSize, 0, 100, "%d%%",
+ ImGuiSliderFlags_AlwaysClamp);
+ ImGui::InputFloat("Arrow Weight", m_pArrowWeight);
+ ImColor col(*m_pArrowColor);
+ if (ImGui::ColorEdit3("Arrow Color", &col.Value.x,
+ ImGuiColorEditFlags_NoInputs)) {
+ *m_pArrowColor = col;
+ }
+ }
+
+ ImGui::Checkbox("Selectable", m_pSelectable);
+}
+
+void ObjectInfo::DrawLine(ImDrawList* drawList,
+ wpi::span<const ImVec2> points) const {
+ if (points.empty()) {
+ return;
+ }
+
+ if (points.size() == 1) {
+ drawList->AddCircleFilled(points.front(), *m_pWeight, *m_pWeight);
+ return;
+ }
+
+ // PolyLine doesn't handle acute angles well; workaround from
+ // https://github.com/ocornut/imgui/issues/3366
+ size_t i = 0;
+ while (i + 1 < points.size()) {
+ int nlin = 2;
+ while (i + nlin < points.size()) {
+ auto [x0, y0] = points[i + nlin - 2];
+ auto [x1, y1] = points[i + nlin - 1];
+ auto [x2, y2] = points[i + nlin];
+ auto s0x = x1 - x0, s0y = y1 - y0;
+ auto s1x = x2 - x1, s1y = y2 - y1;
+ auto dotprod = s1x * s0x + s1y * s0y;
+ if (dotprod < 0) {
+ break;
+ }
+ ++nlin;
+ }
+
+ drawList->AddPolyline(&points[i], nlin, *m_pColor, false, *m_pWeight);
+ i += nlin - 1;
+ }
+
+ if (points.size() > 2 && *m_pStyle == DisplayOptions::kLineClosed) {
+ drawList->AddLine(points.back(), points.front(), *m_pColor, *m_pWeight);
+ }
+}
+
+void ObjectInfo::Reset() {
+ m_texture = gui::Texture{};
+ m_pFilename->clear();
+}
+
+void ObjectInfo::LoadImage() {
+ if (m_fileOpener && m_fileOpener->ready(0)) {
+ auto result = m_fileOpener->result();
+ if (!result.empty()) {
+ LoadImageImpl(result[0].c_str());
+ }
+ m_fileOpener.reset();
+ }
+ if (!m_texture && !m_pFilename->empty()) {
+ if (!LoadImageImpl(m_pFilename->c_str())) {
+ m_pFilename->clear();
+ }
+ }
+}
+
+bool ObjectInfo::LoadImageImpl(const char* fn) {
+ fmt::print("GUI: loading object image '{}'\n", fn);
+ auto texture = gui::Texture::CreateFromFile(fn);
+ if (!texture) {
+ std::fputs("GUI: could not read object image\n", stderr);
+ return false;
+ }
+ m_texture = std::move(texture);
+ *m_pFilename = fn;
+ return true;
+}
+
+PoseFrameData::PoseFrameData(const frc::Pose2d& pose, FieldObjectModel& model,
+ size_t index, const FieldFrameData& ffd,
+ const DisplayOptions& displayOptions)
+ : m_model{model},
+ m_index{index},
+ m_ffd{ffd},
+ m_displayOptions{displayOptions},
+ m_width2(ffd.scale * displayOptions.width / 2),
+ m_length2(ffd.scale * displayOptions.length / 2),
+ m_hitRadius((std::min)(m_width2, m_length2) / 2),
+ m_pose{pose} {
+ UpdateFrameData();
+}
+
+void PoseFrameData::SetPosition(const frc::Translation2d& pos) {
+ m_pose = frc::Pose2d{pos, m_pose.Rotation()};
+ m_model.SetPose(m_index, m_pose);
+}
+
+void PoseFrameData::SetRotation(units::radian_t rot) {
+ m_pose = frc::Pose2d{m_pose.Translation(), rot};
+ m_model.SetPose(m_index, m_pose);
+}
+
+void PoseFrameData::UpdateFrameData() {
+ // (0,0) origin is bottom left
+ ImVec2 center = m_ffd.GetScreenFromPos(m_pose.Translation());
+
+ // build rotated points around center
+ float length2 = m_length2;
+ float width2 = m_width2;
+ auto& rot = GetRotation();
+ float cos_a = rot.Cos();
+ float sin_a = -rot.Sin();
+
+ m_corners[0] = center + ImRotate(ImVec2(-length2, -width2), cos_a, sin_a);
+ m_corners[1] = center + ImRotate(ImVec2(length2, -width2), cos_a, sin_a);
+ m_corners[2] = center + ImRotate(ImVec2(length2, width2), cos_a, sin_a);
+ m_corners[3] = center + ImRotate(ImVec2(-length2, width2), cos_a, sin_a);
+ m_corners[4] = center + ImRotate(ImVec2(0, -width2), cos_a, sin_a);
+ m_corners[5] = center + ImRotate(ImVec2(0, width2), cos_a, sin_a);
+
+ float arrowScale = m_displayOptions.arrowSize / 100.0f;
+ m_arrow[0] =
+ center + ImRotate(ImVec2(-length2 * arrowScale, -width2 * arrowScale),
+ cos_a, sin_a);
+ m_arrow[1] = center + ImRotate(ImVec2(length2 * arrowScale, 0), cos_a, sin_a);
+ m_arrow[2] =
+ center + ImRotate(ImVec2(-length2 * arrowScale, width2 * arrowScale),
+ cos_a, sin_a);
+
+ m_center = center;
+}
+
+std::pair<int, float> PoseFrameData::IsHovered(const ImVec2& cursor) const {
+ float hitRadiusSquared = m_hitRadius * m_hitRadius;
+ float dist;
+
+ // it's within the hit radius of the center?
+ dist = gui::GetDistSquared(cursor, m_center);
+ if (dist < hitRadiusSquared) {
+ return {1, dist};
+ }
+
+ if (m_displayOptions.style == DisplayOptions::kBoxImage) {
+ dist = gui::GetDistSquared(cursor, m_corners[0]);
+ if (dist < hitRadiusSquared) {
+ return {2, dist};
+ }
+
+ dist = gui::GetDistSquared(cursor, m_corners[1]);
+ if (dist < hitRadiusSquared) {
+ return {3, dist};
+ }
+
+ dist = gui::GetDistSquared(cursor, m_corners[2]);
+ if (dist < hitRadiusSquared) {
+ return {4, dist};
+ }
+
+ dist = gui::GetDistSquared(cursor, m_corners[3]);
+ if (dist < hitRadiusSquared) {
+ return {5, dist};
+ }
+ } else if (m_displayOptions.style == DisplayOptions::kTrack) {
+ dist = gui::GetDistSquared(cursor, m_corners[4]);
+ if (dist < hitRadiusSquared) {
+ return {6, dist};
+ }
+
+ dist = gui::GetDistSquared(cursor, m_corners[5]);
+ if (dist < hitRadiusSquared) {
+ return {7, dist};
+ }
+ }
+
+ return {0, 0.0};
+}
+
+SelectedTargetInfo PoseFrameData::GetDragTarget(int corner, float dist) const {
+ SelectedTargetInfo info;
+ info.objModel = &m_model;
+ info.rot = GetRotation().Radians();
+ info.poseCenter = m_center;
+ if (corner == 1) {
+ info.center = m_center;
+ } else {
+ info.center = m_corners[corner - 2];
+ }
+ info.radius = m_hitRadius;
+ info.dist = dist;
+ info.corner = corner;
+ return info;
+}
+
+void PoseFrameData::HandleDrag(const ImVec2& cursor) {
+ if (gDragState.target.corner == 1) {
+ SetPosition(m_ffd.GetPosFromScreen(cursor - gDragState.initialOffset));
+ UpdateFrameData();
+ gDragState.target.center = m_center;
+ gDragState.target.poseCenter = m_center;
+ } else {
+ ImVec2 off = cursor - m_center;
+ SetRotation(gDragState.initialAngle -
+ units::radian_t{std::atan2(off.y, off.x)});
+ gDragState.target.center = m_corners[gDragState.target.corner - 2];
+ gDragState.target.rot = GetRotation().Radians();
+ }
+}
+
+void PoseFrameData::Draw(ImDrawList* drawList, std::vector<ImVec2>* center,
+ std::vector<ImVec2>* left,
+ std::vector<ImVec2>* right) const {
+ switch (m_displayOptions.style) {
+ case DisplayOptions::kBoxImage:
+ if (m_displayOptions.texture) {
+ drawList->AddImageQuad(m_displayOptions.texture, m_corners[0],
+ m_corners[1], m_corners[2], m_corners[3]);
+ return;
+ }
+ drawList->AddQuad(m_corners[0], m_corners[1], m_corners[2], m_corners[3],
+ m_displayOptions.color, m_displayOptions.weight);
+ break;
+ case DisplayOptions::kLine:
+ case DisplayOptions::kLineClosed:
+ center->emplace_back(m_center);
+ break;
+ case DisplayOptions::kTrack:
+ center->emplace_back(m_center);
+ left->emplace_back(m_corners[4]);
+ right->emplace_back(m_corners[5]);
+ break;
+ }
+
+ if (m_displayOptions.arrows) {
+ drawList->AddTriangle(m_arrow[0], m_arrow[1], m_arrow[2],
+ m_displayOptions.arrowColor,
+ m_displayOptions.arrowWeight);
+ }
+}
+
+void glass::DisplayField2DSettings(Field2DModel* model) {
+ auto& storage = GetStorage();
+ auto field = storage.GetData<FieldInfo>();
+ if (!field) {
+ storage.SetData(std::make_shared<FieldInfo>());
+ field = storage.GetData<FieldInfo>();
+ }
+
+ static const char* unitNames[] = {"meters", "feet", "inches"};
+ int* pDisplayUnits = GetStorage().GetIntRef("units", kDisplayMeters);
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
+ ImGui::Combo("Units", pDisplayUnits, unitNames, IM_ARRAYSIZE(unitNames));
+ gDisplayUnits = static_cast<DisplayUnits>(*pDisplayUnits);
+
+ ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
+ if (ImGui::CollapsingHeader("Field")) {
+ ImGui::PushID("Field");
+ field->DisplaySettings();
+ ImGui::PopID();
+ }
+
+ model->ForEachFieldObject([&](auto& objModel, auto name) {
+ if (!objModel.Exists()) {
+ return;
+ }
+ PushID(name);
+ auto& objRef = field->m_objects[name];
+ if (!objRef) {
+ objRef = std::make_unique<ObjectInfo>();
+ }
+ auto obj = objRef.get();
+
+ wpi::SmallString<64> nameBuf{name};
+ if (ImGui::CollapsingHeader(nameBuf.c_str())) {
+ obj->DisplaySettings();
+ }
+ PopID();
+ });
+ ImGui::PopItemWidth();
+}
+
+namespace {
+class FieldDisplay {
+ public:
+ void Display(FieldInfo* field, Field2DModel* model,
+ const ImVec2& contentSize);
+
+ private:
+ void DisplayObject(FieldObjectModel& model, std::string_view name);
+
+ FieldInfo* m_field;
+ ImVec2 m_mousePos;
+ ImDrawList* m_drawList;
+
+ // only allow initiation of dragging when invisible button is hovered;
+ // this prevents the window resize handles from simultaneously activating
+ // the drag functionality
+ bool m_isHovered;
+
+ FieldFrameData m_ffd;
+
+ // drag targets
+ std::vector<SelectedTargetInfo> m_targets;
+
+ // splitter so lines are put behind arrows
+ ImDrawListSplitter m_drawSplit;
+
+ // lines; static so buffer gets reused
+ std::vector<ImVec2> m_centerLine, m_leftLine, m_rightLine;
+};
+} // namespace
+
+void FieldDisplay::Display(FieldInfo* field, Field2DModel* model,
+ const ImVec2& contentSize) {
+ // screen coords
+ ImVec2 cursorPos = ImGui::GetWindowPos() + ImGui::GetCursorPos();
+
+ // for dragging to work, there needs to be a button (otherwise the window is
+ // dragged)
+ ImGui::InvisibleButton("field", contentSize);
+
+ m_field = field;
+ m_mousePos = ImGui::GetIO().MousePos;
+ m_drawList = ImGui::GetWindowDrawList();
+ m_isHovered = ImGui::IsItemHovered();
+
+ // field
+ field->LoadImage();
+ m_ffd = field->GetFrameData(cursorPos, cursorPos + contentSize);
+ field->Draw(m_drawList, m_ffd);
+
+ // stop dragging if mouse button not down
+ bool isDown = ImGui::IsMouseDown(0);
+ if (!isDown) {
+ gDragState.target.objModel = nullptr;
+ }
+
+ // clear popup target if popup closed
+ bool isPopupOpen = ImGui::IsPopupOpen("edit");
+ if (!isPopupOpen) {
+ gPopupState.Close();
+ }
+
+ // field objects
+ m_targets.resize(0);
+ model->ForEachFieldObject([this](auto& objModel, auto name) {
+ if (objModel.Exists()) {
+ DisplayObject(objModel, name);
+ }
+ });
+
+ SelectedTargetInfo* target = nullptr;
+
+ if (gDragState.target.objModel) {
+ target = &gDragState.target;
+ } else if (gPopupState.GetTarget()->objModel) {
+ target = gPopupState.GetTarget();
+ } else if (!m_targets.empty()) {
+ // Find the "best" drag target of the available options. Prefer
+ // center to non-center, and then pick the closest hit.
+ std::sort(m_targets.begin(), m_targets.end(),
+ [](const auto& a, const auto& b) {
+ return a.corner == 0 || a.dist < b.dist;
+ });
+ target = &m_targets.front();
+ }
+
+ if (target) {
+ // draw the target circle; also draw a smaller circle on the pose center
+ m_drawList->AddCircle(target->center, target->radius,
+ IM_COL32(0, 255, 0, 255));
+ if (target->corner != 1) {
+ m_drawList->AddCircle(target->poseCenter, target->radius / 2.0,
+ IM_COL32(0, 255, 0, 255));
+ }
+ }
+
+ // right-click popup for editing
+ if (m_isHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
+ gPopupState.Open(target, m_ffd.GetPosFromScreen(m_mousePos));
+ ImGui::OpenPopup("edit");
+ }
+ if (ImGui::BeginPopup("edit")) {
+ gPopupState.Display(model, m_ffd);
+ ImGui::EndPopup();
+ } else if (target) {
+ if (m_isHovered && ImGui::IsMouseClicked(0)) {
+ // initialize drag state
+ gDragState.target = *target;
+ gDragState.initialOffset = m_mousePos - target->poseCenter;
+ if (target->corner != 1) {
+ gDragState.initialAngle =
+ units::radian_t{std::atan2(gDragState.initialOffset.y,
+ gDragState.initialOffset.x)} +
+ target->rot;
+ }
+ }
+
+ // show tooltip and highlight
+ auto pos = m_ffd.GetPosFromScreen(target->poseCenter);
+ ImGui::SetTooltip(
+ "%s[%d]\nx: %0.3f y: %0.3f rot: %0.3f", target->name.c_str(),
+ static_cast<int>(target->index), ConvertDisplayLength(pos.X()),
+ ConvertDisplayLength(pos.Y()), ConvertDisplayAngle(target->rot));
+ }
+}
+
+void FieldDisplay::DisplayObject(FieldObjectModel& model,
+ std::string_view name) {
+ PushID(name);
+ auto& objRef = m_field->m_objects[name];
+ if (!objRef) {
+ objRef = std::make_unique<ObjectInfo>();
+ }
+ auto obj = objRef.get();
+ obj->LoadImage();
+
+ auto displayOptions = obj->GetDisplayOptions();
+
+ m_centerLine.resize(0);
+ m_leftLine.resize(0);
+ m_rightLine.resize(0);
+
+ m_drawSplit.Split(m_drawList, 2);
+ m_drawSplit.SetCurrentChannel(m_drawList, 1);
+ auto poses = gPopupState.GetInsertModel() == &model
+ ? gPopupState.GetInsertPoses()
+ : model.GetPoses();
+ size_t i = 0;
+ for (auto&& pose : poses) {
+ PoseFrameData pfd{pose, model, i, m_ffd, displayOptions};
+
+ // check for potential drag targets
+ if (displayOptions.selectable && m_isHovered &&
+ !gDragState.target.objModel) {
+ auto [corner, dist] = pfd.IsHovered(m_mousePos);
+ if (corner > 0) {
+ m_targets.emplace_back(pfd.GetDragTarget(corner, dist));
+ m_targets.back().name = name;
+ m_targets.back().index = i;
+ }
+ }
+
+ // handle active dragging of this object
+ if (gDragState.target.objModel == &model && gDragState.target.index == i) {
+ pfd.HandleDrag(m_mousePos);
+ }
+
+ // draw
+ pfd.Draw(m_drawList, &m_centerLine, &m_leftLine, &m_rightLine);
+ ++i;
+ }
+
+ m_drawSplit.SetCurrentChannel(m_drawList, 0);
+ obj->DrawLine(m_drawList, m_centerLine);
+ obj->DrawLine(m_drawList, m_leftLine);
+ obj->DrawLine(m_drawList, m_rightLine);
+ m_drawSplit.Merge(m_drawList);
+
+ PopID();
+}
+
+void PopupState::Open(SelectedTargetInfo* target,
+ const frc::Translation2d& pos) {
+ if (target) {
+ m_target = *target;
+ } else {
+ m_target.objModel = nullptr;
+ m_insertModel = nullptr;
+ m_insertPoses.resize(0);
+ m_insertPoses.emplace_back(pos, 0_deg);
+ m_insertName.clear();
+ m_insertIndex = 0;
+ }
+}
+
+void PopupState::Close() {
+ m_target.objModel = nullptr;
+ m_insertModel = nullptr;
+ m_insertPoses.resize(0);
+}
+
+void PopupState::Display(Field2DModel* model, const FieldFrameData& ffd) {
+ if (m_target.objModel) {
+ DisplayTarget(model, ffd);
+ } else {
+ DisplayInsert(model);
+ }
+}
+
+void PopupState::DisplayTarget(Field2DModel* model, const FieldFrameData& ffd) {
+ ImGui::Text("%s[%d]", m_target.name.c_str(),
+ static_cast<int>(m_target.index));
+ frc::Pose2d pose{ffd.GetPosFromScreen(m_target.poseCenter), m_target.rot};
+ if (InputPose(&pose)) {
+ m_target.poseCenter = ffd.GetScreenFromPos(pose.Translation());
+ m_target.rot = pose.Rotation().Radians();
+ m_target.objModel->SetPose(m_target.index, pose);
+ }
+ if (ImGui::Button("Delete Pose")) {
+ auto posesRef = m_target.objModel->GetPoses();
+ std::vector<frc::Pose2d> poses{posesRef.begin(), posesRef.end()};
+ if (m_target.index < poses.size()) {
+ poses.erase(poses.begin() + m_target.index);
+ m_target.objModel->SetPoses(poses);
+ }
+ ImGui::CloseCurrentPopup();
+ }
+ if (ImGui::Button("Delete Object (ALL Poses)")) {
+ model->RemoveFieldObject(m_target.name);
+ ImGui::CloseCurrentPopup();
+ }
+}
+
+void PopupState::DisplayInsert(Field2DModel* model) {
+ ImGui::TextUnformatted("Insert New Pose");
+
+ InputPose(&m_insertPoses[m_insertIndex]);
+
+ const char* insertName = m_insertModel ? m_insertName.c_str() : "<new>";
+ if (ImGui::BeginCombo("Object", insertName)) {
+ bool selected = !m_insertModel;
+ if (ImGui::Selectable("<new>", selected)) {
+ m_insertModel = nullptr;
+ auto pose = m_insertPoses[m_insertIndex];
+ m_insertPoses.resize(0);
+ m_insertPoses.emplace_back(std::move(pose));
+ m_insertName.clear();
+ m_insertIndex = 0;
+ }
+ if (selected) {
+ ImGui::SetItemDefaultFocus();
+ }
+ model->ForEachFieldObject([&](auto& objModel, auto name) {
+ bool selected = m_insertModel == &objModel;
+ if (ImGui::Selectable(name.data(), selected)) {
+ m_insertModel = &objModel;
+ auto pose = m_insertPoses[m_insertIndex];
+ auto posesRef = objModel.GetPoses();
+ m_insertPoses.assign(posesRef.begin(), posesRef.end());
+ m_insertPoses.emplace_back(std::move(pose));
+ m_insertName = name;
+ m_insertIndex = m_insertPoses.size() - 1;
+ }
+ if (selected) {
+ ImGui::SetItemDefaultFocus();
+ }
+ });
+ ImGui::EndCombo();
+ }
+ if (m_insertModel) {
+ int oldIndex = m_insertIndex;
+ if (ImGui::InputInt("Pos", &m_insertIndex, 1, 5)) {
+ if (m_insertIndex < 0) {
+ m_insertIndex = 0;
+ }
+ size_t size = m_insertPoses.size();
+ if (static_cast<size_t>(m_insertIndex) >= size) {
+ m_insertIndex = size - 1;
+ }
+ if (m_insertIndex < oldIndex) {
+ auto begin = m_insertPoses.begin();
+ std::rotate(begin + m_insertIndex, begin + oldIndex,
+ begin + oldIndex + 1);
+ } else if (m_insertIndex > oldIndex) {
+ auto rbegin = m_insertPoses.rbegin();
+ std::rotate(rbegin + (size - m_insertIndex), rbegin + (size - oldIndex),
+ rbegin + (size - oldIndex - 1));
+ }
+ }
+ } else {
+ ImGui::InputText("Name", &m_insertName);
+ }
+
+ if (ImGui::Button("Apply")) {
+ if (m_insertModel) {
+ m_insertModel->SetPoses(m_insertPoses);
+ } else if (!m_insertName.empty()) {
+ model->AddFieldObject(m_insertName)->SetPoses(m_insertPoses);
+ }
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("Cancel")) {
+ ImGui::CloseCurrentPopup();
+ }
+}
+
+void glass::DisplayField2D(Field2DModel* model, const ImVec2& contentSize) {
+ auto& storage = GetStorage();
+ auto field = storage.GetData<FieldInfo>();
+ if (!field) {
+ storage.SetData(std::make_shared<FieldInfo>());
+ field = storage.GetData<FieldInfo>();
+ }
+
+ if (contentSize.x <= 0 || contentSize.y <= 0) {
+ return;
+ }
+
+ static FieldDisplay display;
+ display.Display(field, model, contentSize);
+}
+
+void Field2DView::Display() {
+ if (ImGui::BeginPopupContextItem()) {
+ DisplayField2DSettings(m_model);
+ ImGui::EndPopup();
+ }
+ DisplayField2D(m_model, ImGui::GetWindowContentRegionMax() -
+ ImGui::GetWindowContentRegionMin());
+}
diff --git a/glass/src/lib/native/cpp/other/Log.cpp b/glass/src/lib/native/cpp/other/Log.cpp
new file mode 100644
index 0000000..9a1d2c5
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/Log.cpp
@@ -0,0 +1,79 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/Log.h"
+
+#include <imgui.h>
+
+using namespace glass;
+
+LogData::LogData(size_t maxLines) : m_maxLines{maxLines} {}
+
+void LogData::Clear() {
+ m_buf.clear();
+ m_lineOffsets.clear();
+ m_lineOffsets.push_back(0);
+}
+
+void LogData::Append(std::string_view msg) {
+ if (m_lineOffsets.size() >= m_maxLines) {
+ Clear();
+ }
+
+ size_t oldSize = m_buf.size();
+ m_buf.append(msg);
+ for (size_t newSize = m_buf.size(); oldSize < newSize; ++oldSize) {
+ if (m_buf[oldSize] == '\n') {
+ m_lineOffsets.push_back(oldSize + 1);
+ }
+ }
+}
+
+const std::string& LogData::GetBuffer() {
+ return m_buf;
+}
+
+void glass::DisplayLog(LogData* data, bool autoScroll) {
+ if (data->m_buf.empty()) {
+ return;
+ }
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
+ const char* buf = data->m_buf.data();
+ const char* bufEnd = buf + data->m_buf.size();
+ ImGuiListClipper clipper;
+ clipper.Begin(data->m_lineOffsets.size());
+ while (clipper.Step()) {
+ for (size_t lineNum = clipper.DisplayStart;
+ lineNum < static_cast<size_t>(clipper.DisplayEnd); lineNum++) {
+ const char* lineStart = buf + data->m_lineOffsets[lineNum];
+ const char* lineEnd = (lineNum + 1 < data->m_lineOffsets.size())
+ ? (buf + data->m_lineOffsets[lineNum + 1] - 1)
+ : bufEnd;
+ ImGui::TextUnformatted(lineStart, lineEnd);
+ }
+ }
+ clipper.End();
+ ImGui::PopStyleVar();
+
+ if (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
+ ImGui::SetScrollHereY(1.0f);
+ }
+}
+
+void LogView::Display() {
+ if (ImGui::BeginPopupContextItem()) {
+ ImGui::Checkbox("Auto-scroll", &m_autoScroll);
+ if (ImGui::Selectable("Clear")) {
+ m_data->Clear();
+ }
+ const auto& buf = m_data->GetBuffer();
+ if (ImGui::Selectable("Copy to Clipboard", false,
+ buf.empty() ? ImGuiSelectableFlags_Disabled : 0)) {
+ ImGui::SetClipboardText(buf.c_str());
+ }
+ ImGui::EndPopup();
+ }
+
+ DisplayLog(m_data, m_autoScroll);
+}
diff --git a/glass/src/lib/native/cpp/other/Mechanism2D.cpp b/glass/src/lib/native/cpp/other/Mechanism2D.cpp
new file mode 100644
index 0000000..07e83e2
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/Mechanism2D.cpp
@@ -0,0 +1,259 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/Mechanism2D.h"
+
+#include <algorithm>
+#include <cmath>
+#include <cstdio>
+#include <memory>
+#include <string_view>
+#include <utility>
+
+#include <fmt/format.h>
+#include <frc/geometry/Pose2d.h>
+#include <frc/geometry/Rotation2d.h>
+#include <frc/geometry/Transform2d.h>
+#include <frc/geometry/Translation2d.h>
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+#include <imgui.h>
+#include <imgui_internal.h>
+#include <imgui_stdlib.h>
+#include <portable-file-dialogs.h>
+#include <units/angle.h>
+#include <units/length.h>
+#include <wpigui.h>
+
+#include "glass/Context.h"
+
+using namespace glass;
+
+namespace gui = wpi::gui;
+
+namespace {
+
+// Per-frame data (not persistent)
+struct FrameData {
+ frc::Translation2d GetPosFromScreen(const ImVec2& cursor) const {
+ return {
+ units::meter_t{(std::clamp(cursor.x, min.x, max.x) - min.x) / scale},
+ units::meter_t{(max.y - std::clamp(cursor.y, min.y, max.y)) / scale}};
+ }
+ ImVec2 GetScreenFromPos(const frc::Translation2d& pos) const {
+ return {min.x + scale * pos.X().to<float>(),
+ max.y - scale * pos.Y().to<float>()};
+ }
+ void DrawObject(ImDrawList* drawList, MechanismObjectModel& objModel,
+ const frc::Pose2d& pose) const;
+ void DrawGroup(ImDrawList* drawList, MechanismObjectGroup& group,
+ const frc::Pose2d& pose) const;
+
+ // in screen coordinates
+ ImVec2 imageMin;
+ ImVec2 imageMax;
+ ImVec2 min;
+ ImVec2 max;
+
+ float scale; // scaling from meters to screen units
+};
+
+class BackgroundInfo {
+ public:
+ BackgroundInfo();
+
+ void DisplaySettings();
+
+ void LoadImage();
+ FrameData GetFrameData(ImVec2 min, ImVec2 max, frc::Translation2d dims) const;
+ void Draw(ImDrawList* drawList, const FrameData& frameData,
+ ImU32 bgColor) const;
+
+ private:
+ void Reset();
+ bool LoadImageImpl(const char* fn);
+
+ std::unique_ptr<pfd::open_file> m_fileOpener;
+
+ std::string* m_pFilename;
+ gui::Texture m_texture;
+
+ // in image pixels
+ int m_imageWidth;
+ int m_imageHeight;
+};
+
+} // namespace
+
+BackgroundInfo::BackgroundInfo() {
+ auto& storage = GetStorage();
+ m_pFilename = storage.GetStringRef("image");
+}
+
+void BackgroundInfo::DisplaySettings() {
+ if (ImGui::Button("Choose image...")) {
+ m_fileOpener = std::make_unique<pfd::open_file>(
+ "Choose background image", "",
+ std::vector<std::string>{"Image File",
+ "*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif "
+ "*.hdr *.pic *.ppm *.pgm"});
+ }
+ if (ImGui::Button("Reset background image")) {
+ Reset();
+ }
+}
+
+void BackgroundInfo::Reset() {
+ m_texture = gui::Texture{};
+ m_pFilename->clear();
+ m_imageWidth = 0;
+ m_imageHeight = 0;
+}
+
+void BackgroundInfo::LoadImage() {
+ if (m_fileOpener && m_fileOpener->ready(0)) {
+ auto result = m_fileOpener->result();
+ if (!result.empty()) {
+ LoadImageImpl(result[0].c_str());
+ }
+ m_fileOpener.reset();
+ }
+ if (!m_texture && !m_pFilename->empty()) {
+ if (!LoadImageImpl(m_pFilename->c_str())) {
+ m_pFilename->clear();
+ }
+ }
+}
+
+bool BackgroundInfo::LoadImageImpl(const char* fn) {
+ fmt::print("GUI: loading background image '{}'\n", fn);
+ auto texture = gui::Texture::CreateFromFile(fn);
+ if (!texture) {
+ std::puts("GUI: could not read background image");
+ return false;
+ }
+ m_texture = std::move(texture);
+ m_imageWidth = m_texture.GetWidth();
+ m_imageHeight = m_texture.GetHeight();
+ *m_pFilename = fn;
+ return true;
+}
+
+FrameData BackgroundInfo::GetFrameData(ImVec2 min, ImVec2 max,
+ frc::Translation2d dims) const {
+ // fit the image into the window
+ if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) {
+ gui::MaxFit(&min, &max, m_imageWidth, m_imageHeight);
+ }
+
+ FrameData frameData;
+ frameData.imageMin = min;
+ frameData.imageMax = max;
+
+ // determine the "active area"
+ float width = dims.X().to<float>();
+ float height = dims.Y().to<float>();
+ gui::MaxFit(&min, &max, width, height);
+
+ frameData.min = min;
+ frameData.max = max;
+ frameData.scale = (max.x - min.x) / width;
+ return frameData;
+}
+
+void BackgroundInfo::Draw(ImDrawList* drawList, const FrameData& frameData,
+ ImU32 bgColor) const {
+ if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) {
+ drawList->AddImage(m_texture, frameData.imageMin, frameData.imageMax);
+ } else {
+ drawList->AddRectFilled(frameData.min, frameData.max, bgColor);
+ }
+}
+
+void glass::DisplayMechanism2DSettings(Mechanism2DModel* model) {
+ auto& storage = GetStorage();
+ auto bg = storage.GetData<BackgroundInfo>();
+ if (!bg) {
+ storage.SetData(std::make_shared<BackgroundInfo>());
+ bg = storage.GetData<BackgroundInfo>();
+ }
+ bg->DisplaySettings();
+}
+
+void FrameData::DrawObject(ImDrawList* drawList, MechanismObjectModel& objModel,
+ const frc::Pose2d& pose) const {
+ const char* type = objModel.GetType();
+ if (std::string_view{type} == "line") {
+ auto startPose =
+ pose + frc::Transform2d{frc::Translation2d{}, objModel.GetAngle()};
+ auto endPose =
+ startPose +
+ frc::Transform2d{frc::Translation2d{objModel.GetLength(), 0_m}, 0_deg};
+ drawList->AddLine(GetScreenFromPos(startPose.Translation()),
+ GetScreenFromPos(endPose.Translation()),
+ objModel.GetColor(), objModel.GetWeight());
+ DrawGroup(drawList, objModel, endPose);
+ }
+}
+
+void FrameData::DrawGroup(ImDrawList* drawList, MechanismObjectGroup& group,
+ const frc::Pose2d& pose) const {
+ group.ForEachObject(
+ [&](auto& objModel) { DrawObject(drawList, objModel, pose); });
+}
+
+void glass::DisplayMechanism2D(Mechanism2DModel* model,
+ const ImVec2& contentSize) {
+ auto& storage = GetStorage();
+ auto bg = storage.GetData<BackgroundInfo>();
+ if (!bg) {
+ storage.SetData(std::make_shared<BackgroundInfo>());
+ bg = storage.GetData<BackgroundInfo>();
+ }
+
+ if (contentSize.x <= 0 || contentSize.y <= 0) {
+ return;
+ }
+
+ // screen coords
+ ImVec2 cursorPos = ImGui::GetWindowPos() + ImGui::GetCursorPos();
+
+ ImGui::InvisibleButton("background", contentSize);
+
+ // auto mousePos = ImGui::GetIO().MousePos;
+ auto drawList = ImGui::GetWindowDrawList();
+ // bool isHovered = ImGui::IsItemHovered();
+
+ // background
+ bg->LoadImage();
+ auto frameData = bg->GetFrameData(cursorPos, cursorPos + contentSize,
+ model->GetDimensions());
+ bg->Draw(drawList, frameData, model->GetBackgroundColor());
+
+ // elements
+ model->ForEachRoot([&](auto& rootModel) {
+ frameData.DrawGroup(drawList, rootModel,
+ frc::Pose2d{rootModel.GetPosition(), 0_deg});
+ });
+
+#if 0
+ if (target) {
+ // show tooltip and highlight
+ auto pos = frameData.GetPosFromScreen(target->poseCenter);
+ ImGui::SetTooltip(
+ "%s[%d]\nx: %0.3f y: %0.3f rot: %0.3f", target->name.c_str(),
+ static_cast<int>(target->index), ConvertDisplayLength(pos.X()),
+ ConvertDisplayLength(pos.Y()), ConvertDisplayAngle(target->rot));
+ }
+#endif
+}
+
+void Mechanism2DView::Display() {
+ if (ImGui::BeginPopupContextItem()) {
+ DisplayMechanism2DSettings(m_model);
+ ImGui::EndPopup();
+ }
+ DisplayMechanism2D(m_model, ImGui::GetWindowContentRegionMax() -
+ ImGui::GetWindowContentRegionMin());
+}
diff --git a/glass/src/lib/native/cpp/other/PIDController.cpp b/glass/src/lib/native/cpp/other/PIDController.cpp
new file mode 100644
index 0000000..3c83b11
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/PIDController.cpp
@@ -0,0 +1,55 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/PIDController.h"
+
+#include <string>
+
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void glass::DisplayPIDController(PIDControllerModel* m) {
+ if (auto name = m->GetName()) {
+ ImGui::Text("%s", name);
+ ImGui::Separator();
+ }
+
+ if (m->Exists()) {
+ auto flag = m->IsReadOnly() ? ImGuiInputTextFlags_ReadOnly
+ : ImGuiInputTextFlags_None;
+ auto createTuningParameter = [flag](const char* name, double* v,
+ std::function<void(double)> callback) {
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
+ if (ImGui::InputDouble(name, v, 0.0, 0.0, "%.3f", flag)) {
+ callback(*v);
+ }
+ };
+
+ if (auto p = m->GetPData()) {
+ double value = p->GetValue();
+ createTuningParameter("P", &value, [=](auto v) { m->SetP(v); });
+ }
+ if (auto i = m->GetIData()) {
+ double value = i->GetValue();
+ createTuningParameter("I", &value, [=](auto v) { m->SetI(v); });
+ }
+ if (auto d = m->GetDData()) {
+ double value = d->GetValue();
+ createTuningParameter("D", &value, [=](auto v) { m->SetD(v); });
+ }
+ if (auto s = m->GetSetpointData()) {
+ double value = s->GetValue();
+ createTuningParameter("Setpoint", &value,
+ [=](auto v) { m->SetSetpoint(v); });
+ }
+ } else {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::Text("Unknown PID Controller");
+ ImGui::PopStyleColor();
+ }
+}
diff --git a/glass/src/lib/native/cpp/other/Plot.cpp b/glass/src/lib/native/cpp/other/Plot.cpp
new file mode 100644
index 0000000..372f8c9
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/Plot.cpp
@@ -0,0 +1,1075 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/Plot.h"
+
+#include <stdint.h>
+
+#include <algorithm>
+#include <atomic>
+#include <cstdio>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <fmt/format.h>
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+#include <imgui.h>
+#include <imgui_internal.h>
+#include <imgui_stdlib.h>
+#include <implot.h>
+#include <wpigui.h>
+#include <wpi/Signal.h>
+#include <wpi/SmallString.h>
+#include <wpi/SmallVector.h>
+#include <wpi/StringExtras.h>
+#include <wpi/timestamp.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+#include "glass/support/ExtraGuiWidgets.h"
+
+using namespace glass;
+
+namespace {
+class PlotView;
+
+struct PlotSeriesRef {
+ PlotView* view;
+ size_t plotIndex;
+ size_t seriesIndex;
+};
+
+class PlotSeries {
+ public:
+ explicit PlotSeries(std::string_view id);
+ explicit PlotSeries(DataSource* source, int yAxis = 0);
+
+ const std::string& GetId() const { return m_id; }
+
+ void CheckSource();
+
+ void SetSource(DataSource* source);
+ DataSource* GetSource() const { return m_source; }
+
+ bool ReadIni(std::string_view name, std::string_view value);
+ void WriteIni(ImGuiTextBuffer* out);
+
+ enum Action { kNone, kMoveUp, kMoveDown, kDelete };
+ Action EmitPlot(PlotView& view, double now, size_t i, size_t plotIndex);
+ void EmitSettings(size_t i);
+ void EmitDragDropPayload(PlotView& view, size_t i, size_t plotIndex);
+
+ const char* GetName() const;
+
+ int GetYAxis() const { return m_yAxis; }
+ void SetYAxis(int yAxis) { m_yAxis = yAxis; }
+
+ private:
+ bool IsDigital() const {
+ return m_digital == kDigital ||
+ (m_digital == kAuto && m_source && m_source->IsDigital());
+ }
+ void AppendValue(double value, uint64_t time);
+
+ // source linkage
+ DataSource* m_source = nullptr;
+ wpi::sig::ScopedConnection m_sourceCreatedConn;
+ wpi::sig::ScopedConnection m_newValueConn;
+ std::string m_id;
+
+ // user settings
+ std::string m_name;
+ int m_yAxis = 0;
+ ImVec4 m_color = IMPLOT_AUTO_COL;
+ int m_marker = 0;
+ float m_weight = IMPLOT_AUTO;
+
+ enum Digital { kAuto, kDigital, kAnalog };
+ int m_digital = 0;
+ int m_digitalBitHeight = 8;
+ int m_digitalBitGap = 4;
+
+ // value storage
+ static constexpr int kMaxSize = 2000;
+ static constexpr double kTimeGap = 0.05;
+ std::atomic<int> m_size = 0;
+ std::atomic<int> m_offset = 0;
+ ImPlotPoint m_data[kMaxSize];
+};
+
+class Plot {
+ public:
+ Plot();
+
+ bool ReadIni(std::string_view name, std::string_view value);
+ void WriteIni(ImGuiTextBuffer* out);
+
+ void DragDropTarget(PlotView& view, size_t i, bool inPlot);
+ void EmitPlot(PlotView& view, double now, bool paused, size_t i);
+ void EmitSettings(size_t i);
+
+ const std::string& GetName() const { return m_name; }
+
+ std::vector<std::unique_ptr<PlotSeries>> m_series;
+
+ // Returns base height; does not include actual plot height if auto-sized.
+ int GetAutoBaseHeight(bool* isAuto, size_t i);
+
+ void SetAutoHeight(int height) {
+ if (m_autoHeight) {
+ m_height = height;
+ }
+ }
+
+ private:
+ void EmitSettingsLimits(int axis);
+
+ std::string m_name;
+ bool m_visible = true;
+ bool m_showPause = true;
+ unsigned int m_plotFlags = ImPlotFlags_None;
+ bool m_lockPrevX = false;
+ bool m_paused = false;
+ float m_viewTime = 10;
+ bool m_autoHeight = true;
+ int m_height = 300;
+ struct PlotRange {
+ double min = 0;
+ double max = 1;
+ bool lockMin = false;
+ bool lockMax = false;
+ bool apply = false;
+ };
+ std::string m_axisLabel[3];
+ PlotRange m_axisRange[3];
+ ImPlotRange m_xaxisRange; // read from plot, used for lockPrevX
+};
+
+class PlotView : public View {
+ public:
+ explicit PlotView(PlotProvider* provider) : m_provider{provider} {}
+
+ void Display() override;
+
+ void MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex);
+
+ void MovePlotSeries(PlotView* fromView, size_t fromPlotIndex,
+ size_t fromSeriesIndex, size_t toPlotIndex,
+ size_t toSeriesIndex, int yAxis = -1);
+
+ PlotProvider* m_provider;
+ std::vector<std::unique_ptr<Plot>> m_plots;
+};
+
+} // namespace
+
+PlotSeries::PlotSeries(std::string_view id) : m_id(id) {
+ if (DataSource* source = DataSource::Find(id)) {
+ SetSource(source);
+ return;
+ }
+ CheckSource();
+}
+
+PlotSeries::PlotSeries(DataSource* source, int yAxis) : m_yAxis(yAxis) {
+ SetSource(source);
+ m_id = source->GetId();
+}
+
+void PlotSeries::CheckSource() {
+ if (!m_newValueConn.connected() && !m_sourceCreatedConn.connected()) {
+ m_source = nullptr;
+ m_sourceCreatedConn = DataSource::sourceCreated.connect_connection(
+ [this](const char* id, DataSource* source) {
+ if (m_id == id) {
+ SetSource(source);
+ m_sourceCreatedConn.disconnect();
+ }
+ });
+ }
+}
+
+void PlotSeries::SetSource(DataSource* source) {
+ m_source = source;
+
+ // add initial value
+ m_data[m_size++] = ImPlotPoint{wpi::Now() * 1.0e-6, source->GetValue()};
+
+ m_newValueConn = source->valueChanged.connect_connection(
+ [this](double value, uint64_t time) { AppendValue(value, time); });
+}
+
+void PlotSeries::AppendValue(double value, uint64_t timeUs) {
+ double time = (timeUs != 0 ? timeUs : wpi::Now()) * 1.0e-6;
+ if (IsDigital()) {
+ if (m_size < kMaxSize) {
+ m_data[m_size] = ImPlotPoint{time, value};
+ ++m_size;
+ } else {
+ m_data[m_offset] = ImPlotPoint{time, value};
+ m_offset = (m_offset + 1) % kMaxSize;
+ }
+ } else {
+ // as an analog graph draws linear lines in between each value,
+ // insert duplicate value if "long" time between updates so it
+ // looks appropriately flat
+ if (m_size < kMaxSize) {
+ if (m_size > 0) {
+ if ((time - m_data[m_size - 1].x) > kTimeGap) {
+ m_data[m_size] = ImPlotPoint{time, m_data[m_size - 1].y};
+ ++m_size;
+ }
+ }
+ m_data[m_size] = ImPlotPoint{time, value};
+ ++m_size;
+ } else {
+ if (m_offset == 0) {
+ if ((time - m_data[kMaxSize - 1].x) > kTimeGap) {
+ m_data[m_offset] = ImPlotPoint{time, m_data[kMaxSize - 1].y};
+ ++m_offset;
+ }
+ } else {
+ if ((time - m_data[m_offset - 1].x) > kTimeGap) {
+ m_data[m_offset] = ImPlotPoint{time, m_data[m_offset - 1].y};
+ m_offset = (m_offset + 1) % kMaxSize;
+ }
+ }
+ m_data[m_offset] = ImPlotPoint{time, value};
+ m_offset = (m_offset + 1) % kMaxSize;
+ }
+ }
+}
+
+bool PlotSeries::ReadIni(std::string_view name, std::string_view value) {
+ if (name == "name") {
+ m_name = value;
+ return true;
+ }
+ if (name == "yAxis") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_yAxis = num.value();
+ }
+ return true;
+ } else if (name == "color") {
+ if (auto num = wpi::parse_integer<unsigned int>(value, 10)) {
+ m_color = ImColor(num.value());
+ }
+ return true;
+ } else if (name == "marker") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_marker = num.value();
+ }
+ return true;
+ } else if (name == "weight") {
+ if (auto num = wpi::parse_float<float>(value)) {
+ m_weight = num.value();
+ }
+ return true;
+ } else if (name == "digital") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_digital = num.value();
+ }
+ return true;
+ } else if (name == "digitalBitHeight") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_digitalBitHeight = num.value();
+ }
+ return true;
+ } else if (name == "digitalBitGap") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_digitalBitGap = num.value();
+ }
+ return true;
+ }
+ return false;
+}
+
+void PlotSeries::WriteIni(ImGuiTextBuffer* out) {
+ out->appendf(
+ "name=%s\nyAxis=%d\ncolor=%u\nmarker=%d\nweight=%f\ndigital=%d\n"
+ "digitalBitHeight=%d\ndigitalBitGap=%d\n",
+ m_name.c_str(), m_yAxis, static_cast<ImU32>(ImColor(m_color)), m_marker,
+ m_weight, m_digital, m_digitalBitHeight, m_digitalBitGap);
+}
+
+const char* PlotSeries::GetName() const {
+ if (!m_name.empty()) {
+ return m_name.c_str();
+ }
+ if (m_newValueConn.connected()) {
+ auto sourceName = m_source->GetName();
+ if (sourceName[0] != '\0') {
+ return sourceName;
+ }
+ }
+ return m_id.c_str();
+}
+
+PlotSeries::Action PlotSeries::EmitPlot(PlotView& view, double now, size_t i,
+ size_t plotIndex) {
+ CheckSource();
+
+ char label[128];
+ std::snprintf(label, sizeof(label), "%s###name", GetName());
+
+ int size = m_size;
+ int offset = m_offset;
+
+ // need to have last value at current time, so need to create fake last value
+ // we handle the offset logic ourselves to avoid wrap issues with size + 1
+ struct GetterData {
+ double now;
+ double zeroTime;
+ ImPlotPoint* data;
+ int size;
+ int offset;
+ };
+ GetterData getterData = {now, GetZeroTime() * 1.0e-6, m_data, size, offset};
+ auto getter = [](void* data, int idx) {
+ auto d = static_cast<GetterData*>(data);
+ if (idx == d->size) {
+ return ImPlotPoint{
+ d->now - d->zeroTime,
+ d->data[d->offset == 0 ? d->size - 1 : d->offset - 1].y};
+ }
+ ImPlotPoint* point;
+ if (d->offset + idx < d->size) {
+ point = &d->data[d->offset + idx];
+ } else {
+ point = &d->data[d->offset + idx - d->size];
+ }
+ return ImPlotPoint{point->x - d->zeroTime, point->y};
+ };
+
+ if (m_color.w == IMPLOT_AUTO_COL.w) {
+ m_color = ImPlot::GetColormapColor(i);
+ }
+ ImPlot::SetNextLineStyle(m_color, m_weight);
+ if (IsDigital()) {
+ ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitHeight, m_digitalBitHeight);
+ ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitGap, m_digitalBitGap);
+ ImPlot::PlotDigitalG(label, getter, &getterData, size + 1);
+ ImPlot::PopStyleVar();
+ ImPlot::PopStyleVar();
+ } else {
+ ImPlot::SetPlotYAxis(m_yAxis);
+ ImPlot::SetNextMarkerStyle(m_marker - 1);
+ ImPlot::PlotLineG(label, getter, &getterData, size + 1);
+ }
+
+ // DND source for PlotSeries
+ if (ImPlot::BeginDragDropSourceItem(label)) {
+ EmitDragDropPayload(view, i, plotIndex);
+ ImPlot::EndDragDropSource();
+ }
+
+ // Show full source name tooltip
+ if (!m_name.empty() && ImPlot::IsLegendEntryHovered(label)) {
+ ImGui::SetTooltip("%s", m_id.c_str());
+ }
+
+ // Edit settings via popup
+ Action rv = kNone;
+ if (ImPlot::BeginLegendPopup(label)) {
+ ImGui::TextUnformatted(m_id.c_str());
+ if (ImGui::Button("Close")) {
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::Text("Edit series name:");
+ ImGui::InputText("##editname", &m_name);
+ if (ImGui::Button("Move Up")) {
+ ImGui::CloseCurrentPopup();
+ rv = kMoveUp;
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("Move Down")) {
+ ImGui::CloseCurrentPopup();
+ rv = kMoveDown;
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("Delete")) {
+ ImGui::CloseCurrentPopup();
+ rv = kDelete;
+ }
+ EmitSettings(i);
+ ImPlot::EndLegendPopup();
+ }
+
+ return rv;
+}
+
+void PlotSeries::EmitDragDropPayload(PlotView& view, size_t i,
+ size_t plotIndex) {
+ PlotSeriesRef ref = {&view, plotIndex, i};
+ ImGui::SetDragDropPayload("PlotSeries", &ref, sizeof(ref));
+ ImGui::TextUnformatted(GetName());
+}
+
+void PlotSeries::EmitSettings(size_t i) {
+ // Line color
+ {
+ ImGui::ColorEdit3("Color", &m_color.x, ImGuiColorEditFlags_NoInputs);
+ ImGui::SameLine();
+ if (ImGui::Button("Default")) {
+ m_color = ImPlot::GetColormapColor(i);
+ }
+ }
+
+ // Line weight
+ {
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
+ ImGui::InputFloat("Weight", &m_weight, 0.1f, 1.0f, "%.1f");
+ }
+
+ // Digital
+ {
+ static const char* const options[] = {"Auto", "Digital", "Analog"};
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
+ ImGui::Combo("Digital", &m_digital, options,
+ sizeof(options) / sizeof(options[0]));
+ }
+
+ if (IsDigital()) {
+ // Bit Height
+ {
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
+ ImGui::InputInt("Bit Height", &m_digitalBitHeight);
+ }
+
+ // Bit Gap
+ {
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
+ ImGui::InputInt("Bit Gap", &m_digitalBitGap);
+ }
+ } else {
+ // Y-axis
+ {
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
+ static const char* const options[] = {"1", "2", "3"};
+ ImGui::Combo("Y-Axis", &m_yAxis, options, 3);
+ }
+
+ // Marker
+ {
+ static const char* const options[] = {
+ "None", "Circle", "Square", "Diamond", "Up", "Down",
+ "Left", "Right", "Cross", "Plus", "Asterisk"};
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
+ ImGui::Combo("Marker", &m_marker, options,
+ sizeof(options) / sizeof(options[0]));
+ }
+ }
+}
+
+Plot::Plot() {
+ for (int i = 0; i < 3; ++i) {
+ m_axisRange[i] = PlotRange{};
+ }
+}
+
+bool Plot::ReadIni(std::string_view name, std::string_view value) {
+ if (name == "name") {
+ m_name = value;
+ return true;
+ } else if (name == "visible") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_visible = num.value() != 0;
+ }
+ return true;
+ } else if (name == "showPause") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_showPause = num.value() != 0;
+ }
+ return true;
+ } else if (name == "lockPrevX") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_lockPrevX = num.value() != 0;
+ }
+ return true;
+ } else if (name == "legend") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ if (num.value() == 0) {
+ m_plotFlags |= ImPlotFlags_NoLegend;
+ } else {
+ m_plotFlags &= ~ImPlotFlags_NoLegend;
+ }
+ }
+ return true;
+ } else if (name == "yaxis2") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ if (num.value() == 0) {
+ m_plotFlags &= ~ImPlotFlags_YAxis2;
+ } else {
+ m_plotFlags |= ImPlotFlags_YAxis2;
+ }
+ }
+ return true;
+ } else if (name == "yaxis3") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ if (num.value() == 0) {
+ m_plotFlags &= ~ImPlotFlags_YAxis3;
+ } else {
+ m_plotFlags |= ImPlotFlags_YAxis3;
+ }
+ }
+ return true;
+ } else if (name == "viewTime") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_viewTime = num.value() / 1000.0;
+ }
+ return true;
+ } else if (name == "autoHeight") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_autoHeight = num.value() != 0;
+ }
+ return true;
+ } else if (name == "height") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_height = num.value();
+ }
+ return true;
+ } else if (wpi::starts_with(name, 'y')) {
+ auto [yAxisStr, yName] = wpi::split(name, '_');
+ int yAxis =
+ wpi::parse_integer<int>(wpi::drop_front(yAxisStr), 10).value_or(-1);
+ if (yAxis < 0 || yAxis > 3) {
+ return false;
+ }
+ if (yName == "min") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_axisRange[yAxis].min = num.value() / 1000.0;
+ }
+ return true;
+ } else if (yName == "max") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_axisRange[yAxis].max = num.value() / 1000.0;
+ }
+ return true;
+ } else if (yName == "lockMin") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_axisRange[yAxis].lockMin = num.value() != 0;
+ }
+ return true;
+ } else if (yName == "lockMax") {
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_axisRange[yAxis].lockMax = num.value() != 0;
+ }
+ return true;
+ } else if (yName == "label") {
+ m_axisLabel[yAxis] = value;
+ return true;
+ }
+ }
+ return false;
+}
+
+void Plot::WriteIni(ImGuiTextBuffer* out) {
+ out->appendf(
+ "name=%s\nvisible=%d\nshowPause=%d\nlockPrevX=%d\nlegend=%d\n"
+ "yaxis2=%d\nyaxis3=%d\nviewTime=%d\nautoHeight=%d\nheight=%d\n",
+ m_name.c_str(), m_visible ? 1 : 0, m_showPause ? 1 : 0,
+ m_lockPrevX ? 1 : 0, (m_plotFlags & ImPlotFlags_NoLegend) ? 0 : 1,
+ (m_plotFlags & ImPlotFlags_YAxis2) ? 1 : 0,
+ (m_plotFlags & ImPlotFlags_YAxis3) ? 1 : 0,
+ static_cast<int>(m_viewTime * 1000), m_autoHeight ? 1 : 0, m_height);
+ for (int i = 0; i < 3; ++i) {
+ out->appendf(
+ "y%d_min=%d\ny%d_max=%d\ny%d_lockMin=%d\ny%d_lockMax=%d\n"
+ "y%d_label=%s\n",
+ i, static_cast<int>(m_axisRange[i].min * 1000), i,
+ static_cast<int>(m_axisRange[i].max * 1000), i,
+ m_axisRange[i].lockMin ? 1 : 0, i, m_axisRange[i].lockMax ? 1 : 0, i,
+ m_axisLabel[i].c_str());
+ }
+}
+
+void Plot::DragDropTarget(PlotView& view, size_t i, bool inPlot) {
+ if (!ImGui::BeginDragDropTarget()) {
+ return;
+ }
+ // handle dragging onto a specific Y axis
+ int yAxis = -1;
+ if (inPlot) {
+ for (int y = 0; y < 3; ++y) {
+ if (ImPlot::IsPlotYAxisHovered(y)) {
+ yAxis = y;
+ break;
+ }
+ }
+ }
+ if (const ImGuiPayload* payload =
+ ImGui::AcceptDragDropPayload("DataSource")) {
+ auto source = *static_cast<DataSource**>(payload->Data);
+ // don't add duplicates unless it's onto a different Y axis
+ auto it =
+ std::find_if(m_series.begin(), m_series.end(), [=](const auto& elem) {
+ return elem->GetId() == source->GetId() &&
+ (yAxis == -1 || elem->GetYAxis() == yAxis);
+ });
+ if (it == m_series.end()) {
+ m_series.emplace_back(
+ std::make_unique<PlotSeries>(source, yAxis == -1 ? 0 : yAxis));
+ }
+ } else if (const ImGuiPayload* payload =
+ ImGui::AcceptDragDropPayload("PlotSeries")) {
+ auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
+ view.MovePlotSeries(ref->view, ref->plotIndex, ref->seriesIndex, i,
+ m_series.size(), yAxis);
+ } else if (const ImGuiPayload* payload =
+ ImGui::AcceptDragDropPayload("Plot")) {
+ auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
+ view.MovePlot(ref->view, ref->plotIndex, i);
+ }
+}
+
+void Plot::EmitPlot(PlotView& view, double now, bool paused, size_t i) {
+ if (!m_visible) {
+ return;
+ }
+
+ bool lockX = (i != 0 && m_lockPrevX);
+
+ if (!lockX && m_showPause && ImGui::Button(m_paused ? "Resume" : "Pause")) {
+ m_paused = !m_paused;
+ }
+
+ char label[128];
+ std::snprintf(label, sizeof(label), "%s##plot", m_name.c_str());
+
+ if (lockX) {
+ ImPlot::SetNextPlotLimitsX(view.m_plots[i - 1]->m_xaxisRange.Min,
+ view.m_plots[i - 1]->m_xaxisRange.Max,
+ ImGuiCond_Always);
+ } else {
+ // also force-pause plots if overall timing is paused
+ double zeroTime = GetZeroTime() * 1.0e-6;
+ ImPlot::SetNextPlotLimitsX(
+ now - zeroTime - m_viewTime, now - zeroTime,
+ (paused || m_paused) ? ImGuiCond_Once : ImGuiCond_Always);
+ }
+
+ ImPlotAxisFlags yFlags[3] = {ImPlotAxisFlags_None,
+ ImPlotAxisFlags_NoGridLines,
+ ImPlotAxisFlags_NoGridLines};
+ for (int i = 0; i < 3; ++i) {
+ ImPlot::SetNextPlotLimitsY(
+ m_axisRange[i].min, m_axisRange[i].max,
+ m_axisRange[i].apply ? ImGuiCond_Always : ImGuiCond_Once, i);
+ m_axisRange[i].apply = false;
+ if (m_axisRange[i].lockMin) {
+ yFlags[i] |= ImPlotAxisFlags_LockMin;
+ }
+ if (m_axisRange[i].lockMax) {
+ yFlags[i] |= ImPlotAxisFlags_LockMax;
+ }
+ }
+
+ if (ImPlot::BeginPlot(
+ label, nullptr,
+ m_axisLabel[0].empty() ? nullptr : m_axisLabel[0].c_str(),
+ ImVec2(-1, m_height), m_plotFlags, ImPlotAxisFlags_None, yFlags[0],
+ yFlags[1], yFlags[2],
+ m_axisLabel[1].empty() ? nullptr : m_axisLabel[1].c_str(),
+ m_axisLabel[2].empty() ? nullptr : m_axisLabel[2].c_str())) {
+ for (size_t j = 0; j < m_series.size(); ++j) {
+ ImGui::PushID(j);
+ switch (m_series[j]->EmitPlot(view, now, j, i)) {
+ case PlotSeries::kMoveUp:
+ if (j > 0) {
+ std::swap(m_series[j - 1], m_series[j]);
+ }
+ break;
+ case PlotSeries::kMoveDown:
+ if (j < (m_series.size() - 1)) {
+ std::swap(m_series[j], m_series[j + 1]);
+ }
+ break;
+ case PlotSeries::kDelete:
+ m_series.erase(m_series.begin() + j);
+ break;
+ default:
+ break;
+ }
+ ImGui::PopID();
+ }
+ DragDropTarget(view, i, true);
+ m_xaxisRange = ImPlot::GetPlotLimits().X;
+ ImPlot::EndPlot();
+ }
+}
+
+void Plot::EmitSettingsLimits(int axis) {
+ ImGui::Indent();
+ ImGui::PushID(axis);
+
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10);
+ ImGui::InputText("Label", &m_axisLabel[axis]);
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
+ ImGui::InputDouble("Min", &m_axisRange[axis].min, 0, 0, "%.3f");
+ ImGui::SameLine();
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
+ ImGui::InputDouble("Max", &m_axisRange[axis].max, 0, 0, "%.3f");
+ ImGui::SameLine();
+ if (ImGui::Button("Apply")) {
+ m_axisRange[axis].apply = true;
+ }
+
+ ImGui::TextUnformatted("Lock Axis");
+ ImGui::SameLine();
+ ImGui::Checkbox("Min##minlock", &m_axisRange[axis].lockMin);
+ ImGui::SameLine();
+ ImGui::Checkbox("Max##maxlock", &m_axisRange[axis].lockMax);
+
+ ImGui::PopID();
+ ImGui::Unindent();
+}
+
+void Plot::EmitSettings(size_t i) {
+ ImGui::Text("Edit plot name:");
+ ImGui::InputText("##editname", &m_name);
+ ImGui::Checkbox("Visible", &m_visible);
+ ImGui::Checkbox("Show Pause Button", &m_showPause);
+ ImGui::CheckboxFlags("Hide Legend", &m_plotFlags, ImPlotFlags_NoLegend);
+ if (i != 0) {
+ ImGui::Checkbox("Lock X-axis to previous plot", &m_lockPrevX);
+ }
+ ImGui::TextUnformatted("Primary Y-Axis");
+ EmitSettingsLimits(0);
+ ImGui::CheckboxFlags("2nd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis2);
+ if ((m_plotFlags & ImPlotFlags_YAxis2) != 0) {
+ EmitSettingsLimits(1);
+ }
+ ImGui::CheckboxFlags("3rd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis3);
+ if ((m_plotFlags & ImPlotFlags_YAxis3) != 0) {
+ EmitSettingsLimits(2);
+ }
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
+ ImGui::InputFloat("View Time (s)", &m_viewTime, 0.1f, 1.0f, "%.1f");
+ ImGui::Checkbox("Auto Height", &m_autoHeight);
+ if (!m_autoHeight) {
+ ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
+ if (ImGui::InputInt("Height", &m_height, 10)) {
+ if (m_height < 0) {
+ m_height = 0;
+ }
+ }
+ }
+}
+
+int Plot::GetAutoBaseHeight(bool* isAuto, size_t i) {
+ *isAuto = m_autoHeight;
+
+ if (!m_visible) {
+ return 0;
+ }
+
+ int height = m_autoHeight ? 0 : m_height;
+
+ // Pause button
+ if ((i == 0 || !m_lockPrevX) && m_showPause) {
+ height += ImGui::GetFrameHeightWithSpacing();
+ }
+
+ return height;
+}
+
+void PlotView::Display() {
+ if (ImGui::BeginPopupContextItem()) {
+ if (ImGui::Button("Add plot")) {
+ m_plots.emplace_back(std::make_unique<Plot>());
+ }
+
+ for (size_t i = 0; i < m_plots.size(); ++i) {
+ auto& plot = m_plots[i];
+ ImGui::PushID(i);
+
+ char name[64];
+ if (!plot->GetName().empty()) {
+ std::snprintf(name, sizeof(name), "%s", plot->GetName().c_str());
+ } else {
+ std::snprintf(name, sizeof(name), "Plot %d", static_cast<int>(i));
+ }
+
+ char label[90];
+ std::snprintf(label, sizeof(label), "%s###header%d", name,
+ static_cast<int>(i));
+
+ bool open = ImGui::CollapsingHeader(label);
+
+ // DND source and target for Plot
+ if (ImGui::BeginDragDropSource()) {
+ PlotSeriesRef ref = {this, i, 0};
+ ImGui::SetDragDropPayload("Plot", &ref, sizeof(ref));
+ ImGui::TextUnformatted(name);
+ ImGui::EndDragDropSource();
+ }
+ plot->DragDropTarget(*this, i, false);
+
+ if (open) {
+ if (ImGui::Button("Move Up")) {
+ if (i > 0) {
+ std::swap(m_plots[i - 1], plot);
+ }
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button("Move Down")) {
+ if (i < (m_plots.size() - 1)) {
+ std::swap(plot, m_plots[i + 1]);
+ }
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button("Delete")) {
+ m_plots.erase(m_plots.begin() + i);
+ ImGui::PopID();
+ continue;
+ }
+
+ plot->EmitSettings(i);
+ }
+
+ ImGui::PopID();
+ }
+
+ ImGui::EndPopup();
+ }
+
+ if (m_plots.empty()) {
+ if (ImGui::Button("Add plot")) {
+ m_plots.emplace_back(std::make_unique<Plot>());
+ }
+
+ // Make "add plot" button a DND target for Plot
+ if (!ImGui::BeginDragDropTarget()) {
+ return;
+ }
+ if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("Plot")) {
+ auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
+ MovePlot(ref->view, ref->plotIndex, 0);
+ }
+ return;
+ }
+
+ // Auto-size plots. This requires two passes: the first pass to get the
+ // total height, the second to actually set the height after averaging it
+ // across all auto-sized heights.
+ int availHeight = ImGui::GetContentRegionAvail().y;
+ int numAuto = 0;
+ for (size_t i = 0; i < m_plots.size(); ++i) {
+ bool isAuto;
+ availHeight -= m_plots[i]->GetAutoBaseHeight(&isAuto, i);
+ availHeight -= ImGui::GetStyle().ItemSpacing.y;
+ if (isAuto) {
+ ++numAuto;
+ }
+ }
+ if (numAuto > 0) {
+ availHeight /= numAuto;
+ for (size_t i = 0; i < m_plots.size(); ++i) {
+ m_plots[i]->SetAutoHeight(availHeight);
+ }
+ }
+
+ double now = wpi::Now() * 1.0e-6;
+ for (size_t i = 0; i < m_plots.size(); ++i) {
+ ImGui::PushID(i);
+ m_plots[i]->EmitPlot(*this, now, m_provider->IsPaused(), i);
+ ImGui::PopID();
+ }
+}
+
+void PlotView::MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex) {
+ if (fromView == this) {
+ if (fromIndex == toIndex) {
+ return;
+ }
+ auto val = std::move(m_plots[fromIndex]);
+ m_plots.insert(m_plots.begin() + toIndex, std::move(val));
+ m_plots.erase(m_plots.begin() + fromIndex + (fromIndex > toIndex ? 1 : 0));
+ } else {
+ auto val = std::move(fromView->m_plots[fromIndex]);
+ m_plots.insert(m_plots.begin() + toIndex, std::move(val));
+ fromView->m_plots.erase(fromView->m_plots.begin() + fromIndex);
+ }
+}
+
+void PlotView::MovePlotSeries(PlotView* fromView, size_t fromPlotIndex,
+ size_t fromSeriesIndex, size_t toPlotIndex,
+ size_t toSeriesIndex, int yAxis) {
+ if (fromView == this && fromPlotIndex == toPlotIndex) {
+ // need to handle this specially as the index of the old location changes
+ if (fromSeriesIndex != toSeriesIndex) {
+ auto& plotSeries = m_plots[fromPlotIndex]->m_series;
+ auto val = std::move(plotSeries[fromSeriesIndex]);
+ // only set Y-axis if actually set
+ if (yAxis != -1) {
+ val->SetYAxis(yAxis);
+ }
+ plotSeries.insert(plotSeries.begin() + toSeriesIndex, std::move(val));
+ plotSeries.erase(plotSeries.begin() + fromSeriesIndex +
+ (fromSeriesIndex > toSeriesIndex ? 1 : 0));
+ }
+ } else {
+ auto& fromPlot = *fromView->m_plots[fromPlotIndex];
+ auto& toPlot = *m_plots[toPlotIndex];
+ // always set Y-axis if moving plots
+ fromPlot.m_series[fromSeriesIndex]->SetYAxis(yAxis == -1 ? 0 : yAxis);
+ toPlot.m_series.insert(toPlot.m_series.begin() + toSeriesIndex,
+ std::move(fromPlot.m_series[fromSeriesIndex]));
+ fromPlot.m_series.erase(fromPlot.m_series.begin() + fromSeriesIndex);
+ }
+}
+
+PlotProvider::PlotProvider(std::string_view iniName)
+ : WindowManager{fmt::format("{}Window", iniName)},
+ m_plotSaver{iniName, this, false},
+ m_seriesSaver{fmt::format("{}Series", iniName), this, true} {}
+
+PlotProvider::~PlotProvider() = default;
+
+void PlotProvider::GlobalInit() {
+ WindowManager::GlobalInit();
+ wpi::gui::AddInit([this] {
+ m_plotSaver.Initialize();
+ m_seriesSaver.Initialize();
+ });
+}
+
+void PlotProvider::DisplayMenu() {
+ for (size_t i = 0; i < m_windows.size(); ++i) {
+ m_windows[i]->DisplayMenuItem();
+ // provide method to destroy the plot window
+ if (ImGui::BeginPopupContextItem()) {
+ if (ImGui::Selectable("Destroy Plot Window")) {
+ m_windows.erase(m_windows.begin() + i);
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::EndPopup();
+ }
+ }
+
+ if (ImGui::MenuItem("New Plot Window")) {
+ // this is an inefficient algorithm, but the number of windows is small
+ char id[32];
+ size_t numWindows = m_windows.size();
+ for (size_t i = 0; i <= numWindows; ++i) {
+ std::snprintf(id, sizeof(id), "Plot <%d>", static_cast<int>(i));
+ bool match = false;
+ for (size_t j = i; j < numWindows; ++j) {
+ if (m_windows[j]->GetId() == id) {
+ match = true;
+ break;
+ }
+ }
+ if (!match) {
+ break;
+ }
+ }
+ if (auto win = AddWindow(id, std::make_unique<PlotView>(this))) {
+ win->SetDefaultSize(700, 400);
+ }
+ }
+}
+
+void PlotProvider::DisplayWindows() {
+ // create views if not already created
+ for (auto&& window : m_windows) {
+ if (!window->HasView()) {
+ window->SetView(std::make_unique<PlotView>(this));
+ }
+ }
+ WindowManager::DisplayWindows();
+}
+
+PlotProvider::IniSaver::IniSaver(std::string_view typeName,
+ PlotProvider* provider, bool forSeries)
+ : IniSaverBase{typeName}, m_provider{provider}, m_forSeries{forSeries} {}
+
+void* PlotProvider::IniSaver::IniReadOpen(const char* name) {
+ auto [viewId, plotNumStr] = wpi::split(name, '#');
+ std::string_view seriesId;
+ if (m_forSeries) {
+ std::tie(plotNumStr, seriesId) = wpi::split(plotNumStr, '#');
+ if (seriesId.empty()) {
+ return nullptr;
+ }
+ }
+ unsigned int plotNum;
+ if (auto plotNumOpt = wpi::parse_integer<unsigned int>(plotNumStr, 10)) {
+ plotNum = plotNumOpt.value();
+ } else {
+ return nullptr;
+ }
+
+ // get or create window
+ auto win = m_provider->GetOrAddWindow(viewId, true);
+ if (!win) {
+ return nullptr;
+ }
+
+ // get or create view
+ auto view = static_cast<PlotView*>(win->GetView());
+ if (!view) {
+ win->SetView(std::make_unique<PlotView>(m_provider));
+ view = static_cast<PlotView*>(win->GetView());
+ }
+
+ // get or create plot
+ if (view->m_plots.size() <= plotNum) {
+ view->m_plots.resize(plotNum + 1);
+ }
+ auto& plot = view->m_plots[plotNum];
+ if (!plot) {
+ plot = std::make_unique<Plot>();
+ }
+
+ // early exit for plot data
+ if (!m_forSeries) {
+ return plot.get();
+ }
+
+ // get or create series
+ return plot->m_series.emplace_back(std::make_unique<PlotSeries>(seriesId))
+ .get();
+}
+
+void PlotProvider::IniSaver::IniReadLine(void* entry, const char* line) {
+ auto [name, value] = wpi::split(line, '=');
+ name = wpi::trim(name);
+ value = wpi::trim(value);
+ if (m_forSeries) {
+ static_cast<PlotSeries*>(entry)->ReadIni(name, value);
+ } else {
+ static_cast<Plot*>(entry)->ReadIni(name, value);
+ }
+}
+
+void PlotProvider::IniSaver::IniWriteAll(ImGuiTextBuffer* out_buf) {
+ for (auto&& win : m_provider->m_windows) {
+ auto view = static_cast<PlotView*>(win->GetView());
+ auto id = win->GetId();
+ for (size_t i = 0; i < view->m_plots.size(); ++i) {
+ if (m_forSeries) {
+ // Loop over series
+ for (auto&& series : view->m_plots[i]->m_series) {
+ out_buf->appendf("[%s][%s#%d#%s]\n", GetTypeName(), id.data(),
+ static_cast<int>(i), series->GetId().c_str());
+ series->WriteIni(out_buf);
+ out_buf->append("\n");
+ }
+ } else {
+ // Just the plot
+ out_buf->appendf("[%s][%s#%d]\n", GetTypeName(), id.data(),
+ static_cast<int>(i));
+ view->m_plots[i]->WriteIni(out_buf);
+ out_buf->append("\n");
+ }
+ }
+ }
+}
diff --git a/glass/src/lib/native/cpp/other/StringChooser.cpp b/glass/src/lib/native/cpp/other/StringChooser.cpp
new file mode 100644
index 0000000..46fa674
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/StringChooser.cpp
@@ -0,0 +1,41 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/StringChooser.h"
+
+#include <imgui.h>
+
+using namespace glass;
+
+void glass::DisplayStringChooser(StringChooserModel* model) {
+ auto& defaultValue = model->GetDefault();
+ auto& selected = model->GetSelected();
+ auto& active = model->GetActive();
+ auto& options = model->GetOptions();
+
+ const char* preview =
+ selected.empty() ? defaultValue.c_str() : selected.c_str();
+
+ const char* label;
+ if (active == preview) {
+ label = "GOOD##select";
+ } else {
+ label = "BAD ##select";
+ }
+
+ if (ImGui::BeginCombo(label, preview)) {
+ for (auto&& option : options) {
+ ImGui::PushID(option.c_str());
+ bool isSelected = (option == selected);
+ if (ImGui::Selectable(option.c_str(), isSelected)) {
+ model->SetSelected(option);
+ }
+ if (isSelected) {
+ ImGui::SetItemDefaultFocus();
+ }
+ ImGui::PopID();
+ }
+ ImGui::EndCombo();
+ }
+}
diff --git a/glass/src/lib/native/cpp/other/Subsystem.cpp b/glass/src/lib/native/cpp/other/Subsystem.cpp
new file mode 100644
index 0000000..c4ed474
--- /dev/null
+++ b/glass/src/lib/native/cpp/other/Subsystem.cpp
@@ -0,0 +1,29 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/other/Subsystem.h"
+
+#include <imgui.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+void glass::DisplaySubsystem(SubsystemModel* m) {
+ if (auto name = m->GetName()) {
+ ImGui::Text("%s", name);
+ ImGui::Separator();
+ }
+ if (m->Exists()) {
+ std::string defaultCommand = m->GetDefaultCommand();
+ std::string currentCommand = m->GetCurrentCommand();
+ ImGui::Text("%s", ("Default Command: " + defaultCommand).c_str());
+ ImGui::Text("%s", ("Current Command: " + currentCommand).c_str());
+ } else {
+ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
+ ImGui::Text("Unknown Subsystem");
+ ImGui::PopStyleColor();
+ }
+}
diff --git a/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp b/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp
new file mode 100644
index 0000000..2af6e5e
--- /dev/null
+++ b/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp
@@ -0,0 +1,177 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/support/ExtraGuiWidgets.h"
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+#include <imgui_internal.h>
+
+#include "glass/DataSource.h"
+
+namespace glass {
+
+void DrawLEDSources(const int* values, DataSource** sources, int numValues,
+ int cols, const ImU32* colors, float size, float spacing,
+ const LEDConfig& config) {
+ if (numValues == 0 || cols < 1) {
+ return;
+ }
+ if (size == 0) {
+ size = ImGui::GetFontSize() / 2.0;
+ }
+ if (spacing == 0) {
+ spacing = ImGui::GetFontSize() / 3.0;
+ }
+
+ int rows = (numValues + cols - 1) / cols;
+ float inc = size + spacing;
+
+ ImDrawList* drawList = ImGui::GetWindowDrawList();
+ const ImVec2 p = ImGui::GetCursorScreenPos();
+
+ float sized2 = size / 2;
+ float ystart, yinc;
+ if (config.start & 1) {
+ // lower
+ ystart = p.y + sized2 + inc * (rows - 1);
+ yinc = -inc;
+ } else {
+ // upper
+ ystart = p.y + sized2;
+ yinc = inc;
+ }
+
+ float xstart, xinc;
+ if (config.start & 2) {
+ // right
+ xstart = p.x + sized2 + inc * (cols - 1);
+ xinc = -inc;
+ } else {
+ // left
+ xstart = p.x + sized2;
+ xinc = inc;
+ }
+
+ float x = xstart, y = ystart;
+ int rowcol = 1; // row for row-major, column for column-major
+ for (int i = 0; i < numValues; ++i) {
+ if (config.order == LEDConfig::RowMajor) {
+ if (i >= (rowcol * cols)) {
+ ++rowcol;
+ if (config.serpentine) {
+ x -= xinc;
+ xinc = -xinc;
+ } else {
+ x = xstart;
+ }
+ y += yinc;
+ }
+ } else {
+ if (i >= (rowcol * rows)) {
+ ++rowcol;
+ if (config.serpentine) {
+ y -= yinc;
+ yinc = -yinc;
+ } else {
+ y = ystart;
+ }
+ x += xinc;
+ }
+ }
+ if (values[i] > 0) {
+ drawList->AddRectFilled(ImVec2(x, y), ImVec2(x + size, y + size),
+ colors[values[i] - 1]);
+ } else if (values[i] < 0) {
+ drawList->AddRect(ImVec2(x, y), ImVec2(x + size, y + size),
+ colors[-values[i] - 1], 0.0f, 0, 1.0);
+ }
+ if (sources) {
+ ImGui::SetCursorScreenPos(ImVec2(x - sized2, y - sized2));
+ if (sources[i]) {
+ ImGui::PushID(i);
+ ImGui::Selectable("", false, 0, ImVec2(inc, inc));
+ sources[i]->EmitDrag();
+ ImGui::PopID();
+ } else {
+ ImGui::Dummy(ImVec2(inc, inc));
+ }
+ }
+ if (config.order == LEDConfig::RowMajor) {
+ x += xinc;
+ } else {
+ y += yinc;
+ }
+ }
+
+ if (!sources) {
+ ImGui::Dummy(ImVec2(inc * cols, inc * rows));
+ }
+}
+
+void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors,
+ float size, float spacing, const LEDConfig& config) {
+ DrawLEDSources(values, nullptr, numValues, cols, colors, size, spacing,
+ config);
+}
+
+bool DeleteButton(ImGuiID id, const ImVec2& pos) {
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+
+ // We intentionally allow interaction when clipped so that a mechanical
+ // Alt,Right,Validate sequence close a window. (this isn't the regular
+ // behavior of buttons, but it doesn't affect the user much because navigation
+ // tends to keep items visible).
+ const ImRect bb(
+ pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f);
+ bool is_clipped = !ImGui::ItemAdd(bb, id);
+
+ bool hovered, held;
+ bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held);
+ if (is_clipped) {
+ return pressed;
+ }
+
+ // Render
+ ImU32 col =
+ ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
+ ImVec2 center = bb.GetCenter();
+ if (hovered) {
+ window->DrawList->AddCircleFilled(
+ center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f), col, 12);
+ }
+
+ ImU32 cross_col = ImGui::GetColorU32(ImGuiCol_Text);
+ window->DrawList->AddCircle(center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f),
+ cross_col, 12);
+ float cross_extent = g.FontSize * 0.5f * 0.5f - 1.0f;
+ center -= ImVec2(0.5f, 0.5f);
+ window->DrawList->AddLine(center + ImVec2(+cross_extent, +cross_extent),
+ center + ImVec2(-cross_extent, -cross_extent),
+ cross_col, 1.0f);
+ window->DrawList->AddLine(center + ImVec2(+cross_extent, -cross_extent),
+ center + ImVec2(-cross_extent, +cross_extent),
+ cross_col, 1.0f);
+
+ return pressed;
+}
+
+bool HeaderDeleteButton(const char* label) {
+ ImGuiWindow* window = ImGui::GetCurrentWindow();
+ ImGuiContext& g = *GImGui;
+ ImGuiLastItemDataBackup last_item_backup;
+ ImGuiID id = window->GetID(label);
+ float button_size = g.FontSize;
+ float button_x = ImMax(window->DC.LastItemRect.Min.x,
+ window->DC.LastItemRect.Max.x -
+ g.Style.FramePadding.x * 2.0f - button_size);
+ float button_y = window->DC.LastItemRect.Min.y;
+ bool rv = DeleteButton(
+ window->GetID(reinterpret_cast<void*>(static_cast<intptr_t>(id) + 1)),
+ ImVec2(button_x, button_y));
+ last_item_backup.Restore();
+ return rv;
+}
+
+} // namespace glass
diff --git a/glass/src/lib/native/cpp/support/IniSaverBase.cpp b/glass/src/lib/native/cpp/support/IniSaverBase.cpp
new file mode 100644
index 0000000..ae8d811
--- /dev/null
+++ b/glass/src/lib/native/cpp/support/IniSaverBase.cpp
@@ -0,0 +1,62 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/support/IniSaverBase.h"
+
+#include <imgui_internal.h>
+
+using namespace glass;
+
+namespace {
+class ImGuiSaver : public IniSaverBackend {
+ public:
+ void Register(IniSaverBase* iniSaver) override;
+ void Unregister(IniSaverBase* iniSaver) override;
+};
+} // namespace
+
+void ImGuiSaver::Register(IniSaverBase* iniSaver) {
+ // hook ini handler to save settings
+ ImGuiSettingsHandler iniHandler;
+ iniHandler.TypeName = iniSaver->GetTypeName();
+ iniHandler.TypeHash = ImHashStr(iniHandler.TypeName);
+ iniHandler.ReadOpenFn = [](ImGuiContext* ctx, ImGuiSettingsHandler* handler,
+ const char* name) {
+ return static_cast<IniSaverBase*>(handler->UserData)->IniReadOpen(name);
+ };
+ iniHandler.ReadLineFn = [](ImGuiContext* ctx, ImGuiSettingsHandler* handler,
+ void* entry, const char* line) {
+ static_cast<IniSaverBase*>(handler->UserData)->IniReadLine(entry, line);
+ };
+ iniHandler.WriteAllFn = [](ImGuiContext* ctx, ImGuiSettingsHandler* handler,
+ ImGuiTextBuffer* out_buf) {
+ static_cast<IniSaverBase*>(handler->UserData)->IniWriteAll(out_buf);
+ };
+ iniHandler.UserData = iniSaver;
+ ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
+}
+
+void ImGuiSaver::Unregister(IniSaverBase* iniSaver) {
+ if (auto ctx = ImGui::GetCurrentContext()) {
+ auto& handlers = ctx->SettingsHandlers;
+ for (auto it = handlers.begin(), end = handlers.end(); it != end; ++it) {
+ if (it->UserData == iniSaver) {
+ handlers.erase(it);
+ return;
+ }
+ }
+ }
+}
+
+static ImGuiSaver* GetSaverInstance() {
+ static ImGuiSaver* inst = new ImGuiSaver;
+ return inst;
+}
+
+IniSaverBase::IniSaverBase(std::string_view typeName, IniSaverBackend* backend)
+ : m_typeName(typeName), m_backend{backend ? backend : GetSaverInstance()} {}
+
+IniSaverBase::~IniSaverBase() {
+ m_backend->Unregister(this);
+}
diff --git a/glass/src/lib/native/cpp/support/IniSaverInfo.cpp b/glass/src/lib/native/cpp/support/IniSaverInfo.cpp
new file mode 100644
index 0000000..6525e8e
--- /dev/null
+++ b/glass/src/lib/native/cpp/support/IniSaverInfo.cpp
@@ -0,0 +1,168 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/support/IniSaverInfo.h"
+
+#include <cstdio>
+#include <cstring>
+
+#include <imgui_internal.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+void NameInfo::SetName(std::string_view name) {
+ size_t len = (std::min)(name.size(), sizeof(m_name) - 1);
+ std::memcpy(m_name, name.data(), len);
+ m_name[len] = '\0';
+}
+
+void NameInfo::GetName(char* buf, size_t size, const char* defaultName) const {
+ if (m_name[0] != '\0') {
+ std::snprintf(buf, size, "%s", m_name);
+ } else {
+ std::snprintf(buf, size, "%s", defaultName);
+ }
+}
+
+void NameInfo::GetName(char* buf, size_t size, const char* defaultName,
+ int index) const {
+ if (m_name[0] != '\0') {
+ std::snprintf(buf, size, "%s [%d]", m_name, index);
+ } else {
+ std::snprintf(buf, size, "%s[%d]", defaultName, index);
+ }
+}
+
+void NameInfo::GetName(char* buf, size_t size, const char* defaultName,
+ int index, int index2) const {
+ if (m_name[0] != '\0') {
+ std::snprintf(buf, size, "%s [%d,%d]", m_name, index, index2);
+ } else {
+ std::snprintf(buf, size, "%s[%d,%d]", defaultName, index, index2);
+ }
+}
+
+void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName) const {
+ if (m_name[0] != '\0') {
+ std::snprintf(buf, size, "%s###Name%s", m_name, defaultName);
+ } else {
+ std::snprintf(buf, size, "%s###Name%s", defaultName, defaultName);
+ }
+}
+
+void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName,
+ int index) const {
+ if (m_name[0] != '\0') {
+ std::snprintf(buf, size, "%s [%d]###Name%d", m_name, index, index);
+ } else {
+ std::snprintf(buf, size, "%s[%d]###Name%d", defaultName, index, index);
+ }
+}
+
+void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName,
+ int index, int index2) const {
+ if (m_name[0] != '\0') {
+ std::snprintf(buf, size, "%s [%d,%d]###Name%d", m_name, index, index2,
+ index);
+ } else {
+ std::snprintf(buf, size, "%s[%d,%d]###Name%d", defaultName, index, index2,
+ index);
+ }
+}
+
+bool NameInfo::ReadIni(std::string_view name, std::string_view value) {
+ if (name != "name") {
+ return false;
+ }
+ size_t len = (std::min)(value.size(), sizeof(m_name) - 1);
+ std::memcpy(m_name, value.data(), len);
+ m_name[len] = '\0';
+ return true;
+}
+
+void NameInfo::WriteIni(ImGuiTextBuffer* out) {
+ out->appendf("name=%s\n", m_name);
+}
+
+void NameInfo::PushEditNameId(int index) {
+ char id[64];
+ std::snprintf(id, sizeof(id), "Name%d", index);
+ ImGui::PushID(id);
+}
+
+void NameInfo::PushEditNameId(const char* name) {
+ char id[128];
+ std::snprintf(id, sizeof(id), "Name%s", name);
+ ImGui::PushID(id);
+}
+
+bool NameInfo::PopupEditName(int index) {
+ bool rv = false;
+ char id[64];
+ std::snprintf(id, sizeof(id), "Name%d", index);
+ if (ImGui::BeginPopupContextItem(id)) {
+ ImGui::Text("Edit name:");
+ if (InputTextName("##edit")) {
+ rv = true;
+ }
+ if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
+ ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::EndPopup();
+ }
+ return rv;
+}
+
+bool NameInfo::PopupEditName(const char* name) {
+ bool rv = false;
+ char id[128];
+ std::snprintf(id, sizeof(id), "Name%s", name);
+ if (ImGui::BeginPopupContextItem(id)) {
+ ImGui::Text("Edit name:");
+ if (InputTextName("##edit")) {
+ rv = true;
+ }
+ if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
+ ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::EndPopup();
+ }
+ return rv;
+}
+
+bool NameInfo::InputTextName(const char* label_id, ImGuiInputTextFlags flags) {
+ return ImGui::InputText(label_id, m_name, sizeof(m_name), flags);
+}
+
+bool OpenInfo::ReadIni(std::string_view name, std::string_view value) {
+ if (name != "open") {
+ return false;
+ }
+ if (auto num = wpi::parse_integer<int>(value, 10)) {
+ m_open = num.value();
+ }
+ return true;
+}
+
+void OpenInfo::WriteIni(ImGuiTextBuffer* out) {
+ out->appendf("open=%d\n", m_open ? 1 : 0);
+}
+
+bool NameOpenInfo::ReadIni(std::string_view name, std::string_view value) {
+ if (NameInfo::ReadIni(name, value)) {
+ return true;
+ }
+ if (OpenInfo::ReadIni(name, value)) {
+ return true;
+ }
+ return false;
+}
+
+void NameOpenInfo::WriteIni(ImGuiTextBuffer* out) {
+ NameInfo::WriteIni(out);
+ OpenInfo::WriteIni(out);
+}