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/app/generate/WPILibVersion.cpp.in b/glass/src/app/generate/WPILibVersion.cpp.in
new file mode 100644
index 0000000..b0a4490
--- /dev/null
+++ b/glass/src/app/generate/WPILibVersion.cpp.in
@@ -0,0 +1,7 @@
+/*
+ * Autogenerated file! Do not manually edit this file. This version is regenerated
+ * any time the publish task is run, or when this file is deleted.
+ */
+const char* GetWPILibVersion() {
+  return "${wpilib_version}";
+}
diff --git a/glass/src/app/native/cpp/main.cpp b/glass/src/app/native/cpp/main.cpp
new file mode 100644
index 0000000..3e0adf1
--- /dev/null
+++ b/glass/src/app/native/cpp/main.cpp
@@ -0,0 +1,218 @@
+// 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 <memory>
+
+#include <GLFW/glfw3.h>
+#include <fmt/format.h>
+#include <imgui.h>
+#include <ntcore_cpp.h>
+#include <wpigui.h>
+
+#include "glass/Context.h"
+#include "glass/Model.h"
+#include "glass/View.h"
+#include "glass/networktables/NetworkTables.h"
+#include "glass/networktables/NetworkTablesProvider.h"
+#include "glass/networktables/NetworkTablesSettings.h"
+#include "glass/other/Log.h"
+#include "glass/other/Plot.h"
+
+namespace gui = wpi::gui;
+
+const char* GetWPILibVersion();
+
+namespace glass {
+std::string_view GetResource_glass_16_png();
+std::string_view GetResource_glass_32_png();
+std::string_view GetResource_glass_48_png();
+std::string_view GetResource_glass_64_png();
+std::string_view GetResource_glass_128_png();
+std::string_view GetResource_glass_256_png();
+std::string_view GetResource_glass_512_png();
+}  // namespace glass
+
+static std::unique_ptr<glass::PlotProvider> gPlotProvider;
+static std::unique_ptr<glass::NetworkTablesProvider> gNtProvider;
+
+static std::unique_ptr<glass::NetworkTablesModel> gNetworkTablesModel;
+static std::unique_ptr<glass::NetworkTablesSettings> gNetworkTablesSettings;
+static glass::LogData gNetworkTablesLog;
+static glass::Window* gNetworkTablesWindow;
+static glass::Window* gNetworkTablesSettingsWindow;
+static glass::Window* gNetworkTablesLogWindow;
+
+static void NtInitialize() {
+  // update window title when connection status changes
+  auto inst = nt::GetDefaultInstance();
+  auto poller = nt::CreateConnectionListenerPoller(inst);
+  nt::AddPolledConnectionListener(poller, true);
+  gui::AddEarlyExecute([poller] {
+    auto win = gui::GetSystemWindow();
+    if (!win) {
+      return;
+    }
+    bool timedOut;
+    for (auto&& event : nt::PollConnectionListener(poller, 0, &timedOut)) {
+      if (event.connected) {
+        glfwSetWindowTitle(
+            win, fmt::format("Glass - Connected ({})", event.conn.remote_ip)
+                     .c_str());
+      } else {
+        glfwSetWindowTitle(win, "Glass - DISCONNECTED");
+      }
+    }
+  });
+
+  // handle NetworkTables log messages
+  auto logPoller = nt::CreateLoggerPoller(inst);
+  nt::AddPolledLogger(logPoller, NT_LOG_INFO, 100);
+  gui::AddEarlyExecute([logPoller] {
+    bool timedOut;
+    for (auto&& msg : nt::PollLogger(logPoller, 0, &timedOut)) {
+      const char* level = "";
+      if (msg.level >= NT_LOG_CRITICAL) {
+        level = "CRITICAL: ";
+      } else if (msg.level >= NT_LOG_ERROR) {
+        level = "ERROR: ";
+      } else if (msg.level >= NT_LOG_WARNING) {
+        level = "WARNING: ";
+      }
+      gNetworkTablesLog.Append(fmt::format("{}{} ({}:{})\n", level, msg.message,
+                                           msg.filename, msg.line));
+    }
+  });
+
+  gNetworkTablesLogWindow = gNtProvider->AddWindow(
+      "NetworkTables Log",
+      std::make_unique<glass::LogView>(&gNetworkTablesLog));
+  if (gNetworkTablesLogWindow) {
+    gNetworkTablesLogWindow->SetDefaultPos(250, 615);
+    gNetworkTablesLogWindow->SetDefaultSize(600, 130);
+    gNetworkTablesLogWindow->SetVisible(false);
+    gNetworkTablesLogWindow->DisableRenamePopup();
+  }
+
+  // NetworkTables table window
+  gNetworkTablesModel = std::make_unique<glass::NetworkTablesModel>();
+  gui::AddEarlyExecute([] { gNetworkTablesModel->Update(); });
+
+  gNetworkTablesWindow = gNtProvider->AddWindow(
+      "NetworkTables",
+      std::make_unique<glass::NetworkTablesView>(gNetworkTablesModel.get()));
+  if (gNetworkTablesWindow) {
+    gNetworkTablesWindow->SetDefaultPos(250, 277);
+    gNetworkTablesWindow->SetDefaultSize(750, 185);
+    gNetworkTablesWindow->DisableRenamePopup();
+  }
+
+  // NetworkTables settings window
+  gNetworkTablesSettings = std::make_unique<glass::NetworkTablesSettings>();
+  gui::AddEarlyExecute([] { gNetworkTablesSettings->Update(); });
+
+  gNetworkTablesSettingsWindow = gNtProvider->AddWindow(
+      "NetworkTables Settings", [] { gNetworkTablesSettings->Display(); });
+  if (gNetworkTablesSettingsWindow) {
+    gNetworkTablesSettingsWindow->SetDefaultPos(30, 30);
+    gNetworkTablesSettingsWindow->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+    gNetworkTablesSettingsWindow->DisableRenamePopup();
+  }
+}
+
+#ifdef _WIN32
+int __stdcall WinMain(void* hInstance, void* hPrevInstance, char* pCmdLine,
+                      int nCmdShow) {
+#else
+int main() {
+#endif
+  gui::CreateContext();
+  glass::CreateContext();
+
+  gui::AddIcon(glass::GetResource_glass_16_png());
+  gui::AddIcon(glass::GetResource_glass_32_png());
+  gui::AddIcon(glass::GetResource_glass_48_png());
+  gui::AddIcon(glass::GetResource_glass_64_png());
+  gui::AddIcon(glass::GetResource_glass_128_png());
+  gui::AddIcon(glass::GetResource_glass_256_png());
+  gui::AddIcon(glass::GetResource_glass_512_png());
+
+  gPlotProvider = std::make_unique<glass::PlotProvider>("Plot");
+  gNtProvider = std::make_unique<glass::NetworkTablesProvider>("NTProvider");
+
+  gui::ConfigurePlatformSaveFile("glass.ini");
+  gPlotProvider->GlobalInit();
+  gui::AddInit([] { glass::ResetTime(); });
+  gNtProvider->GlobalInit();
+  gui::AddInit(NtInitialize);
+
+  glass::AddStandardNetworkTablesViews(*gNtProvider);
+
+  gui::AddLateExecute([] {
+    ImGui::BeginMainMenuBar();
+    gui::EmitViewMenu();
+    if (ImGui::BeginMenu("View")) {
+      if (ImGui::MenuItem("Reset Time")) {
+        glass::ResetTime();
+      }
+      ImGui::EndMenu();
+    }
+    if (ImGui::BeginMenu("NetworkTables")) {
+      if (gNetworkTablesSettingsWindow) {
+        gNetworkTablesSettingsWindow->DisplayMenuItem("NetworkTables Settings");
+      }
+      if (gNetworkTablesWindow) {
+        gNetworkTablesWindow->DisplayMenuItem("NetworkTables View");
+      }
+      if (gNetworkTablesLogWindow) {
+        gNetworkTablesLogWindow->DisplayMenuItem("NetworkTables Log");
+      }
+      ImGui::Separator();
+      gNtProvider->DisplayMenu();
+      ImGui::EndMenu();
+    }
+    if (ImGui::BeginMenu("Plot")) {
+      bool paused = gPlotProvider->IsPaused();
+      if (ImGui::MenuItem("Pause All Plots", nullptr, &paused)) {
+        gPlotProvider->SetPaused(paused);
+      }
+      ImGui::Separator();
+      gPlotProvider->DisplayMenu();
+      ImGui::EndMenu();
+    }
+
+    bool about = false;
+    if (ImGui::BeginMenu("Info")) {
+      if (ImGui::MenuItem("About")) {
+        about = true;
+      }
+      ImGui::EndMenu();
+    }
+    ImGui::EndMainMenuBar();
+
+    if (about) {
+      ImGui::OpenPopup("About");
+      about = false;
+    }
+    if (ImGui::BeginPopupModal("About")) {
+      ImGui::Text("Glass: A different kind of dashboard");
+      ImGui::Separator();
+      ImGui::Text("v%s", GetWPILibVersion());
+      if (ImGui::Button("Close")) {
+        ImGui::CloseCurrentPopup();
+      }
+      ImGui::EndPopup();
+    }
+  });
+
+  gui::Initialize("Glass - DISCONNECTED", 1024, 768);
+  gui::Main();
+
+  gNetworkTablesModel.reset();
+  gNetworkTablesSettings.reset();
+  gNtProvider.reset();
+  gPlotProvider.reset();
+
+  glass::DestroyContext();
+  gui::DestroyContext();
+}
diff --git a/glass/src/app/native/mac/glass.icns b/glass/src/app/native/mac/glass.icns
new file mode 100644
index 0000000..74b6850
--- /dev/null
+++ b/glass/src/app/native/mac/glass.icns
Binary files differ
diff --git a/glass/src/app/native/resources/glass-128.png b/glass/src/app/native/resources/glass-128.png
new file mode 100644
index 0000000..f2ea7a1
--- /dev/null
+++ b/glass/src/app/native/resources/glass-128.png
Binary files differ
diff --git a/glass/src/app/native/resources/glass-16.png b/glass/src/app/native/resources/glass-16.png
new file mode 100644
index 0000000..dbca52a
--- /dev/null
+++ b/glass/src/app/native/resources/glass-16.png
Binary files differ
diff --git a/glass/src/app/native/resources/glass-256.png b/glass/src/app/native/resources/glass-256.png
new file mode 100644
index 0000000..db9062f
--- /dev/null
+++ b/glass/src/app/native/resources/glass-256.png
Binary files differ
diff --git a/glass/src/app/native/resources/glass-32.png b/glass/src/app/native/resources/glass-32.png
new file mode 100644
index 0000000..b6f69d0
--- /dev/null
+++ b/glass/src/app/native/resources/glass-32.png
Binary files differ
diff --git a/glass/src/app/native/resources/glass-48.png b/glass/src/app/native/resources/glass-48.png
new file mode 100644
index 0000000..14ee934
--- /dev/null
+++ b/glass/src/app/native/resources/glass-48.png
Binary files differ
diff --git a/glass/src/app/native/resources/glass-512.png b/glass/src/app/native/resources/glass-512.png
new file mode 100644
index 0000000..b022d1e
--- /dev/null
+++ b/glass/src/app/native/resources/glass-512.png
Binary files differ
diff --git a/glass/src/app/native/resources/glass-64.png b/glass/src/app/native/resources/glass-64.png
new file mode 100644
index 0000000..b488115
--- /dev/null
+++ b/glass/src/app/native/resources/glass-64.png
Binary files differ
diff --git a/glass/src/app/native/win/glass.ico b/glass/src/app/native/win/glass.ico
new file mode 100644
index 0000000..99c5409
--- /dev/null
+++ b/glass/src/app/native/win/glass.ico
Binary files differ
diff --git a/glass/src/app/native/win/glass.rc b/glass/src/app/native/win/glass.rc
new file mode 100644
index 0000000..a9f2951
--- /dev/null
+++ b/glass/src/app/native/win/glass.rc
@@ -0,0 +1 @@
+IDI_ICON1 ICON "glass.ico"
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);
+}
diff --git a/glass/src/lib/native/include/glass/Context.h b/glass/src/lib/native/include/glass/Context.h
new file mode 100644
index 0000000..62b0a33
--- /dev/null
+++ b/glass/src/lib/native/include/glass/Context.h
@@ -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.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include <imgui.h>
+
+namespace glass {
+
+struct Context;
+
+Context* CreateContext();
+void DestroyContext(Context* ctx = nullptr);
+Context* GetCurrentContext();
+void SetCurrentContext(Context* ctx);
+
+/**
+ * Resets zero time to current time.
+ */
+void ResetTime();
+
+/**
+ * Gets the zero time.
+ */
+uint64_t GetZeroTime();
+
+/**
+ * Storage provides both persistent and non-persistent key/value storage for
+ * widgets.
+ *
+ * Keys are always strings.  The storage also provides non-persistent arbitrary
+ * data storage (via std::shared_ptr<void>).
+ *
+ * Storage is automatically indexed internally by the ID stack.  Note it is
+ * necessary to use the glass wrappers for PushID et al to preserve naming in
+ * the save file (unnamed values are still stored, but this is non-ideal for
+ * users trying to hand-edit the save file).
+ */
+class Storage {
+ public:
+  struct Value {
+    Value() = default;
+    explicit Value(std::string_view str) : stringVal{str} {}
+
+    enum Type { kNone, kInt, kInt64, kBool, kFloat, kDouble, kString };
+    Type type = kNone;
+    union {
+      int intVal;
+      int64_t int64Val;
+      bool boolVal;
+      float floatVal;
+      double doubleVal;
+    };
+    std::string stringVal;
+  };
+
+  int GetInt(std::string_view key, int defaultVal = 0) const;
+  int64_t GetInt64(std::string_view key, int64_t defaultVal = 0) const;
+  bool GetBool(std::string_view key, bool defaultVal = false) const;
+  float GetFloat(std::string_view key, float defaultVal = 0.0f) const;
+  double GetDouble(std::string_view key, double defaultVal = 0.0) const;
+  std::string GetString(std::string_view key,
+                        std::string_view defaultVal = {}) const;
+
+  void SetInt(std::string_view key, int val);
+  void SetInt64(std::string_view key, int64_t val);
+  void SetBool(std::string_view key, bool val);
+  void SetFloat(std::string_view key, float val);
+  void SetDouble(std::string_view key, double val);
+  void SetString(std::string_view key, std::string_view val);
+
+  int* GetIntRef(std::string_view key, int defaultVal = 0);
+  int64_t* GetInt64Ref(std::string_view key, int64_t defaultVal = 0);
+  bool* GetBoolRef(std::string_view key, bool defaultVal = false);
+  float* GetFloatRef(std::string_view key, float defaultVal = 0.0f);
+  double* GetDoubleRef(std::string_view key, double defaultVal = 0.0);
+  std::string* GetStringRef(std::string_view key,
+                            std::string_view defaultVal = {});
+
+  Value& GetValue(std::string_view key);
+
+  void SetData(std::shared_ptr<void>&& data) { m_data = std::move(data); }
+
+  template <typename T>
+  T* GetData() const {
+    return static_cast<T*>(m_data.get());
+  }
+
+  Storage() = default;
+  Storage(const Storage&) = delete;
+  Storage& operator=(const Storage&) = delete;
+
+  std::vector<std::string>& GetKeys() { return m_keys; }
+  const std::vector<std::string>& GetKeys() const { return m_keys; }
+  std::vector<std::unique_ptr<Value>>& GetValues() { return m_values; }
+  const std::vector<std::unique_ptr<Value>>& GetValues() const {
+    return m_values;
+  }
+
+ private:
+  mutable std::vector<std::string> m_keys;
+  mutable std::vector<std::unique_ptr<Value>> m_values;
+  std::shared_ptr<void> m_data;
+};
+
+Storage& GetStorage();
+Storage& GetStorage(std::string_view id);
+
+bool Begin(const char* name, bool* p_open = nullptr,
+           ImGuiWindowFlags flags = 0);
+
+void End();
+
+bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0, 0),
+                bool border = false, ImGuiWindowFlags flags = 0);
+
+void EndChild();
+
+/**
+ * Saves open status to storage "open" key.
+ * If returning 'true' the header is open. doesn't indent nor push on ID stack.
+ * user doesn't have to call TreePop().
+ */
+bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0);
+
+bool TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0);
+
+void TreePop();
+
+// push string into the ID stack (will hash string).
+void PushID(const char* str_id);
+
+// push string into the ID stack (will hash string).
+void PushID(const char* str_id_begin, const char* str_id_end);
+
+// push string into the ID stack (will hash string).
+inline void PushID(std::string_view str) {
+  PushID(str.data(), str.data() + str.size());
+}
+
+// push integer into the ID stack (will hash integer).
+void PushID(int int_id);
+
+// pop from the ID stack.
+void PopID();
+
+bool PopupEditName(const char* label, std::string* name);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/ContextInternal.h b/glass/src/lib/native/include/glass/ContextInternal.h
new file mode 100644
index 0000000..39e54f3
--- /dev/null
+++ b/glass/src/lib/native/include/glass/ContextInternal.h
@@ -0,0 +1,49 @@
+// 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.
+
+#pragma once
+
+#include <stdint.h>
+
+#include <memory>
+
+#include <imgui.h>
+#include <wpi/SmallString.h>
+#include <wpi/SmallVector.h>
+#include <wpi/StringMap.h>
+
+#include "glass/Context.h"
+#include "glass/support/IniSaverInfo.h"
+#include "glass/support/IniSaverString.h"
+
+namespace glass {
+
+class DataSource;
+
+class DataSourceName {
+ public:
+  DataSourceName() = default;
+  explicit DataSourceName(DataSource* source) : source{source} {}
+
+  bool ReadIni(std::string_view name_, std::string_view value) {
+    return name->ReadIni(name_, value);
+  }
+  void WriteIni(ImGuiTextBuffer* out) { name->WriteIni(out); }
+
+  std::unique_ptr<NameInfo> name{new NameInfo};
+  DataSource* source = nullptr;
+};
+
+struct Context {
+  wpi::SmallString<128> curId;
+  wpi::SmallVector<size_t, 32> idStack;
+  wpi::StringMap<std::unique_ptr<Storage>> storage;
+  wpi::StringMap<bool> deviceHidden;
+  IniSaverString<DataSourceName> sources{"Data Sources"};
+  uint64_t zeroTime = 0;
+};
+
+extern Context* gContext;
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/DataSource.h b/glass/src/lib/native/include/glass/DataSource.h
new file mode 100644
index 0000000..1d5c37b
--- /dev/null
+++ b/glass/src/lib/native/include/glass/DataSource.h
@@ -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.
+
+#pragma once
+
+#include <stdint.h>
+
+#include <atomic>
+#include <string>
+#include <string_view>
+
+#include <imgui.h>
+#include <wpi/Signal.h>
+#include <wpi/spinlock.h>
+
+namespace glass {
+
+class NameInfo;
+
+/**
+ * A data source for numeric/boolean data.
+ */
+class DataSource {
+ public:
+  explicit DataSource(std::string_view id);
+  DataSource(std::string_view id, int index);
+  DataSource(std::string_view id, int index, int index2);
+  virtual ~DataSource();
+
+  DataSource(const DataSource&) = delete;
+  DataSource& operator=(const DataSource&) = delete;
+
+  const char* GetId() const { return m_id.c_str(); }
+
+  void SetName(std::string_view name);
+  const char* GetName() const;
+  NameInfo& GetNameInfo() { return *m_name; }
+
+  void PushEditNameId(int index);
+  void PushEditNameId(const char* name);
+  bool PopupEditName(int index);
+  bool PopupEditName(const char* name);
+  bool InputTextName(const char* label_id, ImGuiInputTextFlags flags = 0);
+
+  void SetDigital(bool digital) { m_digital = digital; }
+  bool IsDigital() const { return m_digital; }
+
+  void SetValue(double value, uint64_t time = 0) {
+    m_value = value;
+    valueChanged(value, time);
+  }
+  double GetValue() const { return m_value; }
+
+  // drag source helpers
+  void LabelText(const char* label, const char* fmt, ...) const;
+  void LabelTextV(const char* label, const char* fmt, va_list args) const;
+  bool Combo(const char* label, int* current_item, const char* const items[],
+             int items_count, int popup_max_height_in_items = -1) const;
+  bool SliderFloat(const char* label, float* v, float v_min, float v_max,
+                   const char* format = "%.3f", float power = 1.0f) const;
+  bool InputDouble(const char* label, double* v, double step = 0.0,
+                   double step_fast = 0.0, const char* format = "%.6f",
+                   ImGuiInputTextFlags flags = 0) const;
+  bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100,
+                ImGuiInputTextFlags flags = 0) const;
+  void EmitDrag(ImGuiDragDropFlags flags = 0) const;
+
+  wpi::sig::SignalBase<wpi::spinlock, double, uint64_t> valueChanged;
+
+  static DataSource* Find(std::string_view id);
+
+  static wpi::sig::Signal<const char*, DataSource*> sourceCreated;
+
+ private:
+  std::string m_id;
+  NameInfo* m_name;
+  bool m_digital = false;
+  std::atomic<double> m_value = 0;
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/MainMenuBar.h b/glass/src/lib/native/include/glass/MainMenuBar.h
new file mode 100644
index 0000000..7a6a2fc
--- /dev/null
+++ b/glass/src/lib/native/include/glass/MainMenuBar.h
@@ -0,0 +1,48 @@
+// 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.
+
+#pragma once
+
+#include <functional>
+#include <vector>
+
+namespace glass {
+
+class WindowManager;
+
+/**
+ * GUI main menu bar.
+ */
+class MainMenuBar {
+ public:
+  /**
+   * Displays the main menu bar.  Should be added to GUI LateExecute.
+   */
+  void Display();
+
+  /**
+   * Adds to GUI's main menu bar.  The menu function is called from within a
+   * ImGui::BeginMainMenuBar()/EndMainMenuBar() block.  Usually it's only
+   * appropriate to create a menu with ImGui::BeginMenu()/EndMenu() inside of
+   * this function.
+   *
+   * @param menu menu display function
+   */
+  void AddMainMenu(std::function<void()> menu);
+
+  /**
+   * Adds to GUI's option menu.  The menu function is called from within a
+   * ImGui::BeginMenu()/EndMenu() block.  Usually it's only appropriate to
+   * create menu items inside of this function.
+   *
+   * @param menu menu display function
+   */
+  void AddOptionMenu(std::function<void()> menu);
+
+ private:
+  std::vector<std::function<void()>> m_optionMenus;
+  std::vector<std::function<void()>> m_menus;
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/Model.h b/glass/src/lib/native/include/glass/Model.h
new file mode 100644
index 0000000..28f546f
--- /dev/null
+++ b/glass/src/lib/native/include/glass/Model.h
@@ -0,0 +1,22 @@
+// 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.
+
+#pragma once
+
+namespace glass {
+
+class Model {
+ public:
+  Model() = default;
+  virtual ~Model() = default;
+
+  Model(const Model&) = delete;
+  Model& operator=(const Model&) = delete;
+
+  virtual void Update() = 0;
+  virtual bool Exists() = 0;
+  virtual bool IsReadOnly();
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/Provider.h b/glass/src/lib/native/include/glass/Provider.h
new file mode 100644
index 0000000..53b1e75
--- /dev/null
+++ b/glass/src/lib/native/include/glass/Provider.h
@@ -0,0 +1,167 @@
+// 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.
+
+#pragma once
+
+#include <algorithm>
+#include <functional>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include <wpigui.h>
+
+#include "glass/Model.h"
+#include "glass/WindowManager.h"
+
+namespace glass {
+
+namespace detail {
+struct ProviderFunctions {
+  using Exists = std::function<bool()>;
+  using CreateModel = std::function<std::unique_ptr<Model>()>;
+  using ViewExists = std::function<bool(Model*)>;
+  using CreateView = std::function<std::unique_ptr<View>(Window*, Model*)>;
+};
+}  // namespace detail
+
+/**
+ * Providers are registries of models and views.  They have ownership over
+ * their created Models, Windows, and Views.
+ *
+ * GlobalInit() configures Update() to be called during EarlyExecute.
+ * Calling Update() calls Update() on all created models (Provider
+ * implementations must ensure this occurs).
+ *
+ * @tparam Functions defines functor interface types
+ */
+template <typename Functions = detail::ProviderFunctions>
+class Provider : public WindowManager {
+ public:
+  using ExistsFunc = typename Functions::Exists;
+  using CreateModelFunc = typename Functions::CreateModel;
+  using ViewExistsFunc = typename Functions::ViewExists;
+  using CreateViewFunc = typename Functions::CreateView;
+
+  /**
+   * Constructor.
+   *
+   * @param iniName Group name to use in ini file
+   */
+  explicit Provider(std::string_view iniName) : WindowManager{iniName} {}
+
+  Provider(const Provider&) = delete;
+  Provider& operator=(const Provider&) = delete;
+
+  /**
+   * Perform global initialization.  This should be called prior to
+   * wpi::gui::Initialize().
+   */
+  void GlobalInit() override;
+
+  /**
+   * Show the specified view by default on first load.  Has no effect if
+   * the user previously hid the window (e.g. in a saved prior execution).
+   *
+   * @param name View name
+   */
+  void ShowDefault(std::string_view name);
+
+  /**
+   * Register a model and view combination.  Equivalent to calling both
+   * RegisterModel() and RegisterView() with no ViewExistsFunc.
+   *
+   * @param name View/model name
+   * @param exists Functor, returns true if model can be created
+   * @param createModel Functor for creating model
+   * @param createView Functor for creating view
+   */
+  void Register(std::string_view name, ExistsFunc exists,
+                CreateModelFunc createModel, CreateViewFunc createView);
+
+  /**
+   * Register a model.
+   *
+   * @param name Model name
+   * @param exists Functor, returns true if model can be created
+   * @param createModel Functor for creating model
+   */
+  void RegisterModel(std::string_view name, ExistsFunc exists,
+                     CreateModelFunc createModel);
+
+  /**
+   * Register a view.
+   *
+   * @param name View name
+   * @param modelName Model name
+   * @param exists Functor, returns true if view can be created
+   * @param createView Functor for creating view
+   */
+  void RegisterView(std::string_view name, std::string_view modelName,
+                    ViewExistsFunc exists, CreateViewFunc createView);
+
+ protected:
+  virtual void Update();
+
+  struct ModelEntry {
+    ModelEntry(std::string_view name, ExistsFunc exists,
+               CreateModelFunc createModel)
+        : name{name},
+          exists{std::move(exists)},
+          createModel{std::move(createModel)} {}
+    virtual ~ModelEntry() = default;
+
+    std::string name;
+    ExistsFunc exists;
+    CreateModelFunc createModel;
+    std::unique_ptr<Model> model;
+  };
+
+  struct ViewEntry {
+    ViewEntry(std::string_view name, ModelEntry* modelEntry,
+              ViewExistsFunc exists, CreateViewFunc createView)
+        : name{name},
+          modelEntry{modelEntry},
+          exists{std::move(exists)},
+          createView{std::move(createView)} {}
+    virtual ~ViewEntry() = default;
+
+    std::string name;
+    ModelEntry* modelEntry;
+    ViewExistsFunc exists;
+    CreateViewFunc createView;
+    Window* window = nullptr;
+  };
+
+  // sorted by name
+  using ModelEntries = std::vector<std::unique_ptr<ModelEntry>>;
+  ModelEntries m_modelEntries;
+  using ViewEntries = std::vector<std::unique_ptr<ViewEntry>>;
+  ViewEntries m_viewEntries;
+
+  typename ModelEntries::iterator FindModelEntry(std::string_view name);
+  typename ViewEntries::iterator FindViewEntry(std::string_view name);
+
+  virtual std::unique_ptr<ModelEntry> MakeModelEntry(
+      std::string_view name, ExistsFunc exists, CreateModelFunc createModel) {
+    return std::make_unique<ModelEntry>(name, std::move(exists),
+                                        std::move(createModel));
+  }
+
+  virtual std::unique_ptr<ViewEntry> MakeViewEntry(std::string_view name,
+                                                   ModelEntry* modelEntry,
+                                                   ViewExistsFunc exists,
+                                                   CreateViewFunc createView) {
+    return std::make_unique<ViewEntry>(name, modelEntry, std::move(exists),
+                                       std::move(createView));
+  }
+
+  virtual void Show(ViewEntry* entry, Window* window) = 0;
+};
+
+}  // namespace glass
+
+#include "Provider.inc"
diff --git a/glass/src/lib/native/include/glass/Provider.inc b/glass/src/lib/native/include/glass/Provider.inc
new file mode 100644
index 0000000..33bb6e0
--- /dev/null
+++ b/glass/src/lib/native/include/glass/Provider.inc
@@ -0,0 +1,102 @@
+// 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.
+
+#pragma once
+
+#include <utility>
+
+#include "glass/Provider.h"
+
+namespace glass {
+
+template <typename Functions>
+void Provider<Functions>::GlobalInit() {
+  WindowManager::GlobalInit();
+  wpi::gui::AddEarlyExecute([this] { Update(); });
+}
+
+template <typename Functions>
+void Provider<Functions>::ShowDefault(std::string_view name) {
+  auto win = GetWindow(name);
+  if (win) {
+    return;
+  }
+  auto it = FindViewEntry(name);
+  if (it == m_viewEntries.end() || (*it)->name != name) {
+    return;
+  }
+  this->Show(it->get(), (*it)->window);
+}
+
+template <typename Functions>
+void Provider<Functions>::Register(std::string_view name, ExistsFunc exists,
+                                   CreateModelFunc createModel,
+                                   CreateViewFunc createView) {
+  RegisterModel(name, std::move(exists), std::move(createModel));
+  RegisterView(name, name, nullptr, std::move(createView));
+}
+
+template <typename Functions>
+void Provider<Functions>::RegisterModel(std::string_view name,
+                                        ExistsFunc exists,
+                                        CreateModelFunc createModel) {
+  auto it = FindModelEntry(name);
+  // ignore if exists
+  if (it != m_modelEntries.end() && (*it)->name == name) {
+    return;
+  }
+  // insert in sorted location
+  m_modelEntries.emplace(
+      it, MakeModelEntry(name, std::move(exists), std::move(createModel)));
+}
+
+template <typename Functions>
+void Provider<Functions>::RegisterView(std::string_view name,
+                                       std::string_view modelName,
+                                       ViewExistsFunc exists,
+                                       CreateViewFunc createView) {
+  // find model; if model doesn't exist, ignore
+  auto modelIt = FindModelEntry(modelName);
+  if (modelIt == m_modelEntries.end() || (*modelIt)->name != modelName) {
+    return;
+  }
+
+  auto viewIt = FindViewEntry(name);
+  // ignore if exists
+  if (viewIt != m_viewEntries.end() && (*viewIt)->name == name) {
+    return;
+  }
+  // insert in sorted location
+  m_viewEntries.emplace(viewIt,
+                        MakeViewEntry(name, modelIt->get(), std::move(exists),
+                                      std::move(createView)));
+}
+
+template <typename Functions>
+void Provider<Functions>::Update() {
+  // update entries
+  for (auto&& entry : m_modelEntries) {
+    if (entry->model) {
+      entry->model->Update();
+    }
+  }
+}
+
+template <typename Functions>
+typename Provider<Functions>::ModelEntries::iterator
+Provider<Functions>::FindModelEntry(std::string_view name) {
+  return std::lower_bound(
+      m_modelEntries.begin(), m_modelEntries.end(), name,
+      [](const auto& elem, std::string_view s) { return elem->name < s; });
+}
+
+template <typename Functions>
+typename Provider<Functions>::ViewEntries::iterator
+Provider<Functions>::FindViewEntry(std::string_view name) {
+  return std::lower_bound(
+      m_viewEntries.begin(), m_viewEntries.end(), name,
+      [](const auto& elem, std::string_view s) { return elem->name < s; });
+}
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/View.h b/glass/src/lib/native/include/glass/View.h
new file mode 100644
index 0000000..886c29e
--- /dev/null
+++ b/glass/src/lib/native/include/glass/View.h
@@ -0,0 +1,48 @@
+// 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.
+
+#pragma once
+
+#include <memory>
+
+#include <wpi/FunctionExtras.h>
+
+namespace glass {
+
+/**
+ * A view is the contents of a window (1:1 mapping).
+ * It may reference multiple models.
+ *
+ * Typically a view is constructed by a Provider and the View's constructor
+ * is given the corresponding Model(s).
+ *
+ * A view may retain a reference to its parent window for dynamic
+ * window configuration.
+ */
+class View {
+ public:
+  virtual ~View() = default;
+
+  /**
+   * Displays the window contents.  Called by Window::Display() from within an
+   * ImGui::Begin() / ImGui::End() block.
+   */
+  virtual void Display() = 0;
+
+  /**
+   * Called instead of Display() when the window is hidden (e.g. when
+   * ImGui::Begin() returns false).
+   */
+  virtual void Hidden();
+};
+
+/**
+ * Make a View for a display functor.
+ *
+ * @param display Display function
+ * @return unique_ptr to View
+ */
+std::unique_ptr<View> MakeFunctionView(wpi::unique_function<void()> display);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/Window.h b/glass/src/lib/native/include/glass/Window.h
new file mode 100644
index 0000000..780479a
--- /dev/null
+++ b/glass/src/lib/native/include/glass/Window.h
@@ -0,0 +1,132 @@
+// 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.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+
+#include <imgui.h>
+
+#include "glass/View.h"
+
+namespace glass {
+
+/**
+ * Managed window information.
+ * A Window owns the View that displays the window's contents.
+ */
+class Window {
+ public:
+  Window() = default;
+  explicit Window(std::string_view id) : m_id{id}, m_defaultName{id} {}
+
+  std::string_view GetId() const { return m_id; }
+
+  enum Visibility { kHide = 0, kShow, kDisabled };
+
+  bool HasView() { return static_cast<bool>(m_view); }
+
+  void SetView(std::unique_ptr<View> view) { m_view = std::move(view); }
+
+  View* GetView() { return m_view.get(); }
+  const View* GetView() const { return m_view.get(); }
+
+  bool IsVisible() const { return m_visible; }
+  void SetVisible(bool visible) { m_visible = visible; }
+  bool IsEnabled() const { return m_enabled; }
+  void SetEnabled(bool enabled) { m_enabled = enabled; }
+
+  void SetFlags(ImGuiWindowFlags flags) { m_flags = flags; }
+
+  void SetName(std::string_view name) { m_name = name; }
+  void SetDefaultName(std::string_view name) { m_defaultName = name; }
+
+  /**
+   * Normally windows provide a right-click popup menu on the title bar to
+   * rename the window.  Calling this disables that functionality so the
+   * view can provide its own popup.
+   */
+  void DisableRenamePopup() { m_renamePopupEnabled = false; }
+
+  /**
+   * Sets visibility of window.
+   *
+   * @param visibility 0=hide, 1=show, 2=disabled (force-hide)
+   */
+  void SetVisibility(Visibility visibility);
+
+  /**
+   * Sets default position of window.
+   *
+   * @param x x location of upper left corner
+   * @param y y location of upper left corner
+   */
+  void SetDefaultPos(float x, float y) {
+    m_posCond = ImGuiCond_FirstUseEver;
+    m_pos = ImVec2{x, y};
+  }
+
+  /**
+   * Sets default size of window.
+   *
+   * @param width width
+   * @param height height
+   */
+  void SetDefaultSize(float width, float height) {
+    m_sizeCond = ImGuiCond_FirstUseEver;
+    m_size = ImVec2{width, height};
+  }
+
+  /**
+   * Sets internal padding of window.
+   * @param x horizontal padding
+   * @param y vertical padding
+   */
+  void SetPadding(float x, float y) {
+    m_setPadding = true;
+    m_padding = ImVec2{x, y};
+  }
+
+  /**
+   * Displays window.
+   */
+  void Display();
+
+  /**
+   * Displays menu item for the window.
+   * @param label what to display as the menu item label; defaults to
+   *              window ID if nullptr
+   * @return True if window went from invisible to visible.
+   */
+  bool DisplayMenuItem(const char* label = nullptr);
+
+  /**
+   * Scale default window position and size.
+   */
+  void ScaleDefault(float scale);
+
+  void IniReadLine(const char* lineStr);
+  void IniWriteAll(const char* typeName, ImGuiTextBuffer* out_buf);
+
+ private:
+  std::string m_id;
+  std::string m_name;
+  std::string m_defaultName;
+  std::unique_ptr<View> m_view;
+  ImGuiWindowFlags m_flags = 0;
+  bool m_visible = true;
+  bool m_enabled = true;
+  bool m_renamePopupEnabled = true;
+  ImGuiCond m_posCond = 0;
+  ImGuiCond m_sizeCond = 0;
+  ImVec2 m_pos;
+  ImVec2 m_size;
+  bool m_setPadding = false;
+  ImVec2 m_padding;
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/WindowManager.h b/glass/src/lib/native/include/glass/WindowManager.h
new file mode 100644
index 0000000..4024e15
--- /dev/null
+++ b/glass/src/lib/native/include/glass/WindowManager.h
@@ -0,0 +1,137 @@
+// 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.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <string_view>
+#include <type_traits>
+#include <vector>
+
+#include <imgui.h>
+#include <wpi/FunctionExtras.h>
+
+#include "glass/Window.h"
+#include "glass/support/IniSaverBase.h"
+
+namespace glass {
+
+/**
+ * Window manager.
+ *
+ * To properly integrate into an application:
+ * - Call GlobalInit() from the application main, after calling
+ *   wpi::gui::CreateContext(), but before calling wpi::gui::Initialize().
+ * - Add DisplayMenu() to the application's MainMenuBar.
+ */
+class WindowManager {
+ public:
+  /**
+   * Constructor.
+   *
+   * @param iniName Group name to use in ini file
+   */
+  explicit WindowManager(std::string_view iniName);
+  virtual ~WindowManager() = default;
+
+  WindowManager(const WindowManager&) = delete;
+  WindowManager& operator=(const WindowManager&) = delete;
+
+  /**
+   * Perform global initialization.  This should be called prior to
+   * wpi::gui::Initialize().
+   */
+  virtual void GlobalInit();
+
+  /**
+   * Displays menu contents, one item for each window.
+   * See Window::DisplayMenuItem().
+   */
+  virtual void DisplayMenu();
+
+  /**
+   * Adds window to GUI.  The display function is called from within a
+   * ImGui::Begin()/End() block.  While windows can be created within the
+   * execute function passed to gui::AddExecute(), using this function ensures
+   * the windows are consistently integrated with the rest of the GUI.
+   *
+   * On each Dear ImGui frame, gui::AddExecute() functions are always called
+   * prior to AddWindow display functions.  Note that windows may be shaded or
+   * completely hidden, in which case this function will not be called.
+   * It's important to perform any processing steps that must be performed
+   * every frame in the gui::AddExecute() function.
+   *
+   * @param id unique identifier of the window (title bar)
+   * @param display window contents display function
+   */
+  Window* AddWindow(std::string_view id, wpi::unique_function<void()> display);
+
+  /**
+   * Adds window to GUI.  The view's display function is called from within a
+   * ImGui::Begin()/End() block.  While windows can be created within the
+   * execute function passed to gui::AddExecute(), using this function ensures
+   * the windows are consistently integrated with the rest of the GUI.
+   *
+   * On each Dear ImGui frame, gui::AddExecute() functions are always called
+   * prior to AddWindow display functions.  Note that windows may be shaded or
+   * completely hidden, in which case this function will not be called.
+   * It's important to perform any processing steps that must be performed
+   * every frame in the gui::AddExecute() function.
+   *
+   * @param id unique identifier of the window (title bar)
+   * @param view view object
+   * @return Window, or nullptr on duplicate window
+   */
+  Window* AddWindow(std::string_view id, std::unique_ptr<View> view);
+
+  /**
+   * Adds window to GUI.  A View must be assigned to the returned Window
+   * to display the window contents.  While windows can be created within the
+   * execute function passed to gui::AddExecute(), using this function ensures
+   * the windows are consistently integrated with the rest of the GUI.
+   *
+   * On each Dear ImGui frame, gui::AddExecute() functions are always called
+   * prior to AddWindow display functions.  Note that windows may be shaded or
+   * completely hidden, in which case this function will not be called.
+   * It's important to perform any processing steps that must be performed
+   * every frame in the gui::AddExecute() function.
+   *
+   * @param id unique identifier of the window (default title bar)
+   * @return Window, or nullptr on duplicate window
+   */
+  Window* GetOrAddWindow(std::string_view id, bool duplicateOk = false);
+
+  /**
+   * Gets existing window.  If none exists, returns nullptr.
+   *
+   * @param id unique identifier of the window (default title bar)
+   * @return Window, or nullptr if window does not exist
+   */
+  Window* GetWindow(std::string_view id);
+
+ protected:
+  virtual void DisplayWindows();
+
+  // kept sorted by id
+  std::vector<std::unique_ptr<Window>> m_windows;
+
+ private:
+  class IniSaver : public IniSaverBase {
+   public:
+    explicit IniSaver(std::string_view typeName, WindowManager* manager)
+        : IniSaverBase{typeName}, m_manager{manager} {}
+
+    void* IniReadOpen(const char* name) override;
+    void IniReadLine(void* entry, const char* lineStr) override;
+    void IniWriteAll(ImGuiTextBuffer* out_buf) override;
+
+   private:
+    WindowManager* m_manager;
+  };
+
+  IniSaver m_iniSaver;
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/Accelerometer.h b/glass/src/lib/native/include/glass/hardware/Accelerometer.h
new file mode 100644
index 0000000..e997963
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/Accelerometer.h
@@ -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.
+
+#pragma once
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+class AccelerometerModel : public Model {
+ public:
+  virtual DataSource* GetXData() = 0;
+  virtual DataSource* GetYData() = 0;
+  virtual DataSource* GetZData() = 0;
+
+  virtual int GetRange() = 0;
+
+  virtual void SetX(double val) = 0;
+  virtual void SetY(double val) = 0;
+  virtual void SetZ(double val) = 0;
+  virtual void SetRange(int val) = 0;
+};
+
+void DisplayAccelerometerDevice(AccelerometerModel* model);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/AnalogGyro.h b/glass/src/lib/native/include/glass/hardware/AnalogGyro.h
new file mode 100644
index 0000000..fdfcf4f
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/AnalogGyro.h
@@ -0,0 +1,33 @@
+// 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.
+
+#pragma once
+
+#include <wpi/function_ref.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+class AnalogGyroModel : public Model {
+ public:
+  virtual DataSource* GetAngleData() = 0;
+  virtual DataSource* GetRateData() = 0;
+
+  virtual void SetAngle(double val) = 0;
+  virtual void SetRate(double val) = 0;
+};
+
+class AnalogGyrosModel : public Model {
+ public:
+  virtual void ForEachAnalogGyro(
+      wpi::function_ref<void(AnalogGyroModel& model, int index)> func) = 0;
+};
+
+void DisplayAnalogGyroDevice(AnalogGyroModel* model, int index);
+void DisplayAnalogGyrosDevice(AnalogGyrosModel* model);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/AnalogInput.h b/glass/src/lib/native/include/glass/hardware/AnalogInput.h
new file mode 100644
index 0000000..2e49b9c
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/AnalogInput.h
@@ -0,0 +1,37 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+
+#include <wpi/function_ref.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+class AnalogInputModel : public Model {
+ public:
+  virtual bool IsGyro() const = 0;
+  virtual const char* GetSimDevice() const = 0;
+
+  virtual DataSource* GetVoltageData() = 0;
+
+  virtual void SetVoltage(double val) = 0;
+};
+
+class AnalogInputsModel : public Model {
+ public:
+  virtual void ForEachAnalogInput(
+      wpi::function_ref<void(AnalogInputModel& model, int index)> func) = 0;
+};
+
+void DisplayAnalogInput(AnalogInputModel* model, int index);
+void DisplayAnalogInputs(AnalogInputsModel* model,
+                         std::string_view noneMsg = "No analog inputs");
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/AnalogOutput.h b/glass/src/lib/native/include/glass/hardware/AnalogOutput.h
new file mode 100644
index 0000000..7ba8be6
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/AnalogOutput.h
@@ -0,0 +1,30 @@
+// 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.
+
+#pragma once
+
+#include <wpi/function_ref.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+class AnalogOutputModel : public Model {
+ public:
+  virtual DataSource* GetVoltageData() = 0;
+
+  virtual void SetVoltage(double val) = 0;
+};
+
+class AnalogOutputsModel : public Model {
+ public:
+  virtual void ForEachAnalogOutput(
+      wpi::function_ref<void(AnalogOutputModel& model, int index)> func) = 0;
+};
+
+void DisplayAnalogOutputsDevice(AnalogOutputsModel* model);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/DIO.h b/glass/src/lib/native/include/glass/hardware/DIO.h
new file mode 100644
index 0000000..5593ba6
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/DIO.h
@@ -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.
+
+#pragma once
+
+#include <string_view>
+
+#include <wpi/function_ref.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class EncoderModel;
+class DataSource;
+
+class DPWMModel : public Model {
+ public:
+  virtual const char* GetSimDevice() const = 0;
+
+  virtual DataSource* GetValueData() = 0;
+
+  virtual void SetValue(double val) = 0;
+};
+
+class DutyCycleModel : public Model {
+ public:
+  virtual const char* GetSimDevice() const = 0;
+
+  virtual DataSource* GetValueData() = 0;
+
+  virtual void SetValue(double val) = 0;
+};
+
+class DIOModel : public Model {
+ public:
+  virtual const char* GetName() const = 0;
+
+  virtual const char* GetSimDevice() const = 0;
+
+  virtual DPWMModel* GetDPWM() = 0;
+  virtual DutyCycleModel* GetDutyCycle() = 0;
+  virtual EncoderModel* GetEncoder() = 0;
+
+  virtual bool IsInput() const = 0;
+
+  virtual DataSource* GetValueData() = 0;
+
+  virtual void SetValue(bool val) = 0;
+};
+
+class DIOsModel : public Model {
+ public:
+  virtual void ForEachDIO(
+      wpi::function_ref<void(DIOModel& model, int index)> func) = 0;
+};
+
+void DisplayDIO(DIOModel* model, int index, bool outputsEnabled);
+void DisplayDIOs(DIOsModel* model, bool outputsEnabled,
+                 std::string_view noneMsg = "No Digital I/O");
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/Encoder.h b/glass/src/lib/native/include/glass/hardware/Encoder.h
new file mode 100644
index 0000000..41d781a
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/Encoder.h
@@ -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.
+
+#pragma once
+
+#include <string_view>
+
+#include <wpi/function_ref.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+class EncoderModel : public Model {
+ public:
+  virtual void SetName(std::string_view name);
+
+  virtual const char* GetSimDevice() const = 0;
+
+  virtual int GetChannelA() const = 0;
+  virtual int GetChannelB() const = 0;
+
+  virtual DataSource* GetDistancePerPulseData() = 0;
+  virtual DataSource* GetCountData() = 0;
+  virtual DataSource* GetPeriodData() = 0;
+  virtual DataSource* GetDirectionData() = 0;
+  virtual DataSource* GetDistanceData() = 0;
+  virtual DataSource* GetRateData() = 0;
+
+  virtual double GetMaxPeriod() = 0;
+  virtual bool GetReverseDirection() = 0;
+
+  virtual void SetDistancePerPulse(double val) = 0;
+  virtual void SetCount(int val) = 0;
+  virtual void SetPeriod(double val) = 0;
+  virtual void SetDirection(bool val) = 0;
+  virtual void SetDistance(double val) = 0;
+  virtual void SetRate(double val) = 0;
+
+  virtual void SetMaxPeriod(double val) = 0;
+  virtual void SetReverseDirection(bool val) = 0;
+};
+
+class EncodersModel : public Model {
+ public:
+  virtual void ForEachEncoder(
+      wpi::function_ref<void(EncoderModel& model, int index)> func) = 0;
+};
+
+void DisplayEncoder(EncoderModel* model);
+void DisplayEncoders(EncodersModel* model,
+                     std::string_view noneMsg = "No encoders");
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/Gyro.h b/glass/src/lib/native/include/glass/hardware/Gyro.h
new file mode 100644
index 0000000..d2bb09d
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/Gyro.h
@@ -0,0 +1,20 @@
+// 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.
+
+#pragma once
+
+#include "glass/Model.h"
+
+namespace glass {
+class DataSource;
+class GyroModel : public Model {
+ public:
+  virtual const char* GetName() const = 0;
+  virtual const char* GetSimDevice() const = 0;
+
+  virtual DataSource* GetAngleData() = 0;
+  virtual void SetAngle(double angle) = 0;
+};
+void DisplayGyro(GyroModel* m);
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/LEDDisplay.h b/glass/src/lib/native/include/glass/hardware/LEDDisplay.h
new file mode 100644
index 0000000..ddd3c27
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/LEDDisplay.h
@@ -0,0 +1,44 @@
+// 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.
+
+#pragma once
+
+#include <wpi/function_ref.h>
+#include <wpi/span.h>
+
+#include "glass/Model.h"
+
+namespace wpi {
+template <typename T>
+class SmallVectorImpl;
+}  // namespace wpi
+
+namespace glass {
+
+class LEDDisplayModel : public glass::Model {
+ public:
+  struct Data {
+    uint8_t b;
+    uint8_t g;
+    uint8_t r;
+    uint8_t padding;
+  };
+
+  virtual bool IsRunning() = 0;
+
+  virtual wpi::span<const Data> GetData(wpi::SmallVectorImpl<Data>& buf) = 0;
+};
+
+class LEDDisplaysModel : public glass::Model {
+ public:
+  virtual size_t GetNumLEDDisplays() = 0;
+
+  virtual void ForEachLEDDisplay(
+      wpi::function_ref<void(LEDDisplayModel& model, int index)> func) = 0;
+};
+
+void DisplayLEDDisplay(LEDDisplayModel* model, int index);
+void DisplayLEDDisplays(LEDDisplaysModel* model);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/PCM.h b/glass/src/lib/native/include/glass/hardware/PCM.h
new file mode 100644
index 0000000..107a2a8
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/PCM.h
@@ -0,0 +1,60 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+
+#include <wpi/function_ref.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+class CompressorModel : public Model {
+ public:
+  virtual DataSource* GetRunningData() = 0;
+  virtual DataSource* GetEnabledData() = 0;
+  virtual DataSource* GetPressureSwitchData() = 0;
+  virtual DataSource* GetCurrentData() = 0;
+
+  virtual void SetRunning(bool val) = 0;
+  virtual void SetEnabled(bool val) = 0;
+  virtual void SetPressureSwitch(bool val) = 0;
+  virtual void SetCurrent(double val) = 0;
+};
+
+class SolenoidModel : public Model {
+ public:
+  virtual DataSource* GetOutputData() = 0;
+
+  virtual void SetOutput(bool val) = 0;
+};
+
+class PCMModel : public Model {
+ public:
+  virtual CompressorModel* GetCompressor() = 0;
+
+  virtual void ForEachSolenoid(
+      wpi::function_ref<void(SolenoidModel& model, int index)> func) = 0;
+};
+
+class PCMsModel : public Model {
+ public:
+  virtual void ForEachPCM(
+      wpi::function_ref<void(PCMModel& model, int index)> func) = 0;
+};
+
+bool DisplayPCMSolenoids(PCMModel* model, int index, bool outputsEnabled);
+void DisplayPCMsSolenoids(PCMsModel* model, bool outputsEnabled,
+                          std::string_view noneMsg = "No solenoids");
+
+void DisplayCompressorDevice(PCMModel* model, int index, bool outputsEnabled);
+void DisplayCompressorDevice(CompressorModel* model, int index,
+                             bool outputsEnabled);
+void DisplayCompressorsDevice(PCMsModel* model, bool outputsEnabled);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/PWM.h b/glass/src/lib/native/include/glass/hardware/PWM.h
new file mode 100644
index 0000000..74c7461
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/PWM.h
@@ -0,0 +1,37 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+
+#include <wpi/function_ref.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+class PWMModel : public Model {
+ public:
+  // returns -1 if not an addressable LED
+  virtual int GetAddressableLED() const = 0;
+
+  virtual DataSource* GetSpeedData() = 0;
+
+  virtual void SetSpeed(double val) = 0;
+};
+
+class PWMsModel : public Model {
+ public:
+  virtual void ForEachPWM(
+      wpi::function_ref<void(PWMModel& model, int index)> func) = 0;
+};
+
+void DisplayPWM(PWMModel* model, int index, bool outputsEnabled);
+void DisplayPWMs(PWMsModel* model, bool outputsEnabled,
+                 std::string_view noneMsg = "No PWM outputs");
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/PowerDistribution.h b/glass/src/lib/native/include/glass/hardware/PowerDistribution.h
new file mode 100644
index 0000000..0ef6006
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/PowerDistribution.h
@@ -0,0 +1,42 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+
+#include <wpi/function_ref.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+class PowerDistributionModel : public Model {
+ public:
+  virtual int GetNumChannels() const = 0;
+
+  virtual DataSource* GetTemperatureData() = 0;
+  virtual DataSource* GetVoltageData() = 0;
+  virtual DataSource* GetCurrentData(int channel) = 0;
+
+  virtual void SetTemperature(double val) = 0;
+  virtual void SetVoltage(double val) = 0;
+  virtual void SetCurrent(int channel, double val) = 0;
+};
+
+class PowerDistributionsModel : public Model {
+ public:
+  virtual void ForEachPowerDistribution(
+      wpi::function_ref<void(PowerDistributionModel& model, int index)>
+          func) = 0;
+};
+
+void DisplayPowerDistribution(PowerDistributionModel* model, int index);
+void DisplayPowerDistributions(
+    PowerDistributionsModel* model,
+    std::string_view noneMsg = "No Power Distributions");
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/Relay.h b/glass/src/lib/native/include/glass/hardware/Relay.h
new file mode 100644
index 0000000..b025119
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/Relay.h
@@ -0,0 +1,36 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+
+#include <wpi/function_ref.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+class RelayModel : public Model {
+ public:
+  virtual DataSource* GetForwardData() = 0;
+  virtual DataSource* GetReverseData() = 0;
+
+  virtual void SetForward(bool val) = 0;
+  virtual void SetReverse(bool val) = 0;
+};
+
+class RelaysModel : public Model {
+ public:
+  virtual void ForEachRelay(
+      wpi::function_ref<void(RelayModel& model, int index)> func) = 0;
+};
+
+void DisplayRelay(RelayModel* model, int index, bool outputsEnabled);
+void DisplayRelays(RelaysModel* model, bool outputsEnabled,
+                   std::string_view noneMsg = "No relays");
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/RoboRio.h b/glass/src/lib/native/include/glass/hardware/RoboRio.h
new file mode 100644
index 0000000..df9a2a5
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/RoboRio.h
@@ -0,0 +1,45 @@
+// 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.
+
+#pragma once
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+class RoboRioRailModel : public Model {
+ public:
+  virtual DataSource* GetVoltageData() = 0;
+  virtual DataSource* GetCurrentData() = 0;
+  virtual DataSource* GetActiveData() = 0;
+  virtual DataSource* GetFaultsData() = 0;
+
+  virtual void SetVoltage(double val) = 0;
+  virtual void SetCurrent(double val) = 0;
+  virtual void SetActive(bool val) = 0;
+  virtual void SetFaults(int val) = 0;
+};
+
+class RoboRioModel : public Model {
+ public:
+  virtual RoboRioRailModel* GetUser6VRail() = 0;
+  virtual RoboRioRailModel* GetUser5VRail() = 0;
+  virtual RoboRioRailModel* GetUser3V3Rail() = 0;
+
+  virtual DataSource* GetUserButton() = 0;
+  virtual DataSource* GetVInVoltageData() = 0;
+  virtual DataSource* GetVInCurrentData() = 0;
+  virtual DataSource* GetBrownoutVoltage() = 0;
+
+  virtual void SetUserButton(bool val) = 0;
+  virtual void SetVInVoltage(double val) = 0;
+  virtual void SetVInCurrent(double val) = 0;
+  virtual void SetBrownoutVoltage(double val) = 0;
+};
+
+void DisplayRoboRio(RoboRioModel* model);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/hardware/SpeedController.h b/glass/src/lib/native/include/glass/hardware/SpeedController.h
new file mode 100644
index 0000000..033f27d
--- /dev/null
+++ b/glass/src/lib/native/include/glass/hardware/SpeedController.h
@@ -0,0 +1,19 @@
+// 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.
+
+#pragma once
+
+#include "glass/Model.h"
+
+namespace glass {
+class DataSource;
+class SpeedControllerModel : public Model {
+ public:
+  virtual const char* GetName() const = 0;
+  virtual const char* GetSimDevice() const = 0;
+  virtual DataSource* GetPercentData() = 0;
+  virtual void SetPercent(double value) = 0;
+};
+void DisplaySpeedController(SpeedControllerModel* m);
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/CommandScheduler.h b/glass/src/lib/native/include/glass/other/CommandScheduler.h
new file mode 100644
index 0000000..66b92b2
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/CommandScheduler.h
@@ -0,0 +1,21 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "glass/Model.h"
+
+namespace glass {
+class DataSource;
+class CommandSchedulerModel : public Model {
+ public:
+  virtual const char* GetName() const = 0;
+  virtual const std::vector<std::string>& GetCurrentCommands() = 0;
+  virtual void CancelCommand(size_t index) = 0;
+};
+void DisplayCommandScheduler(CommandSchedulerModel* m);
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/CommandSelector.h b/glass/src/lib/native/include/glass/other/CommandSelector.h
new file mode 100644
index 0000000..e5126f2
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/CommandSelector.h
@@ -0,0 +1,18 @@
+// 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.
+
+#pragma once
+
+#include "glass/Model.h"
+
+namespace glass {
+class DataSource;
+class CommandSelectorModel : public Model {
+ public:
+  virtual const char* GetName() const = 0;
+  virtual DataSource* GetRunningData() = 0;
+  virtual void SetRunning(bool run) = 0;
+};
+void DisplayCommandSelector(CommandSelectorModel* m);
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/DeviceTree.h b/glass/src/lib/native/include/glass/other/DeviceTree.h
new file mode 100644
index 0000000..ddd27f2
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/DeviceTree.h
@@ -0,0 +1,140 @@
+// 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.
+
+#pragma once
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include <imgui.h>
+#include <wpi/FunctionExtras.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class DataSource;
+
+/**
+ * Model for device tree.
+ */
+class DeviceTreeModel : public Model {
+ public:
+  using DisplayFunc = wpi::unique_function<void(Model*)>;
+
+  /**
+   * Add a display to the device tree.
+   *
+   * @param model Model to keep updated (may be nullptr)
+   * @param display Display function
+   */
+  void Add(std::unique_ptr<Model> model, DisplayFunc display) {
+    m_displays.emplace_back(model.get(), std::move(display));
+    m_ownedModels.emplace_back(std::move(model));
+  }
+
+  void Add(Model* model, DisplayFunc display) {
+    m_displays.emplace_back(model, std::move(display));
+  }
+
+  void Update() override;
+
+  bool Exists() override;
+
+  void Display();
+
+ private:
+  std::vector<std::pair<Model*, DisplayFunc>> m_displays;
+  std::vector<std::unique_ptr<Model>> m_ownedModels;
+};
+
+/**
+ * Hides device on tree.
+ *
+ * @param id device name
+ */
+void HideDevice(const char* id);
+
+/**
+ * Wraps CollapsingHeader() to provide both hiding functionality and open
+ * persistence.  As with the ImGui function, returns true if the tree node
+ * is visible and expanded.  If returns true, call EndDevice() to finish
+ * the block.
+ *
+ * @param id label
+ * @param flags ImGuiTreeNodeFlags flags
+ * @return True if expanded
+ */
+bool BeginDevice(const char* id, ImGuiTreeNodeFlags flags = 0);
+
+/**
+ * Finish a device block started with BeginDevice().
+ */
+void EndDevice();
+
+/**
+ * Displays device value.
+ *
+ * @param name value name
+ * @param readonly prevent value from being modified by the user
+ * @param value value contents (modified in place)
+ * @param source data source for drag source (may be nullptr)
+ * @return True if value was modified by the user
+ */
+bool DeviceBoolean(const char* name, bool readonly, bool* value,
+                   const DataSource* source = nullptr);
+
+/**
+ * Displays device value.
+ *
+ * @param name value name
+ * @param readonly prevent value from being modified by the user
+ * @param value value contents (modified in place)
+ * @param source data source for drag source (may be nullptr)
+ * @return True if value was modified by the user
+ */
+bool DeviceDouble(const char* name, bool readonly, double* value,
+                  const DataSource* source = nullptr);
+
+/**
+ * Displays device value.
+ *
+ * @param name value name
+ * @param readonly prevent value from being modified by the user
+ * @param value value contents (modified in place)
+ * @param options options array
+ * @param numOptions size of options array
+ * @param source data source for drag source (may be nullptr)
+ * @return True if value was modified by the user
+ */
+bool DeviceEnum(const char* name, bool readonly, int* value,
+                const char** options, int32_t numOptions,
+                const DataSource* source = nullptr);
+
+/**
+ * Displays device value.
+ *
+ * @param name value name
+ * @param readonly prevent value from being modified by the user
+ * @param value value contents (modified in place)
+ * @param source data source for drag source (may be nullptr)
+ * @return True if value was modified by the user
+ */
+bool DeviceInt(const char* name, bool readonly, int32_t* value,
+               const DataSource* source = nullptr);
+
+/**
+ * Displays device value.
+ *
+ * @param name value name
+ * @param readonly prevent value from being modified by the user
+ * @param value value contents (modified in place)
+ * @param source data source for drag source (may be nullptr)
+ * @return True if value was modified by the user
+ */
+bool DeviceLong(const char* name, bool readonly, int64_t* value,
+                const DataSource* source = nullptr);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/Drive.h b/glass/src/lib/native/include/glass/other/Drive.h
new file mode 100644
index 0000000..c969582
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/Drive.h
@@ -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.
+
+#pragma once
+
+#include <functional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "glass/Model.h"
+
+struct ImVec2;
+
+namespace glass {
+class DataSource;
+class DriveModel : public Model {
+ public:
+  struct WheelInfo {
+    std::string name;
+    DataSource* percent;
+    std::function<void(double)> setter;
+
+    WheelInfo(std::string_view name, DataSource* percent,
+              std::function<void(double)> setter)
+        : name(name), percent(percent), setter(std::move(setter)) {}
+  };
+
+  virtual const char* GetName() const = 0;
+  virtual const std::vector<WheelInfo>& GetWheels() const = 0;
+
+  virtual ImVec2 GetSpeedVector() const = 0;
+
+  // Clamped between -1 and 1 with -1 being full CCW.
+  virtual double GetRotation() const = 0;
+};
+void DisplayDrive(DriveModel* m);
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/FMS.h b/glass/src/lib/native/include/glass/other/FMS.h
new file mode 100644
index 0000000..1e0f8ef
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/FMS.h
@@ -0,0 +1,53 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+
+#include "glass/Model.h"
+
+namespace wpi {
+template <typename T>
+class SmallVectorImpl;
+}  // namespace wpi
+
+namespace glass {
+
+class DataSource;
+
+class FMSModel : public Model {
+ public:
+  virtual DataSource* GetFmsAttachedData() = 0;
+  virtual DataSource* GetDsAttachedData() = 0;
+  virtual DataSource* GetAllianceStationIdData() = 0;
+  virtual DataSource* GetMatchTimeData() = 0;
+  virtual DataSource* GetEStopData() = 0;
+  virtual DataSource* GetEnabledData() = 0;
+  virtual DataSource* GetTestData() = 0;
+  virtual DataSource* GetAutonomousData() = 0;
+  virtual std::string_view GetGameSpecificMessage(
+      wpi::SmallVectorImpl<char>& buf) = 0;
+
+  virtual void SetFmsAttached(bool val) = 0;
+  virtual void SetDsAttached(bool val) = 0;
+  virtual void SetAllianceStationId(int val) = 0;
+  virtual void SetMatchTime(double val) = 0;
+  virtual void SetEStop(bool val) = 0;
+  virtual void SetEnabled(bool val) = 0;
+  virtual void SetTest(bool val) = 0;
+  virtual void SetAutonomous(bool val) = 0;
+  virtual void SetGameSpecificMessage(const char* val) = 0;
+};
+
+/**
+ * Displays FMS view.
+ *
+ * @param matchTimeEnabled If not null, a checkbox is displayed for
+ *                         "enable match time" linked to this value
+ */
+void DisplayFMS(FMSModel* model, bool* matchTimeEnabled = nullptr);
+void DisplayFMSReadOnly(FMSModel* model);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/Field2D.h b/glass/src/lib/native/include/glass/other/Field2D.h
new file mode 100644
index 0000000..9399876
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/Field2D.h
@@ -0,0 +1,54 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+
+#include <frc/geometry/Pose2d.h>
+#include <frc/geometry/Rotation2d.h>
+#include <frc/geometry/Translation2d.h>
+#include <imgui.h>
+#include <wpi/function_ref.h>
+#include <wpi/span.h>
+
+#include "glass/Model.h"
+#include "glass/View.h"
+
+namespace glass {
+
+class FieldObjectModel : public Model {
+ public:
+  virtual const char* GetName() const = 0;
+
+  virtual wpi::span<const frc::Pose2d> GetPoses() = 0;
+  virtual void SetPoses(wpi::span<const frc::Pose2d> poses) = 0;
+  virtual void SetPose(size_t i, frc::Pose2d pose) = 0;
+  virtual void SetPosition(size_t i, frc::Translation2d pos) = 0;
+  virtual void SetRotation(size_t i, frc::Rotation2d rot) = 0;
+};
+
+class Field2DModel : public Model {
+ public:
+  virtual FieldObjectModel* AddFieldObject(std::string_view name) = 0;
+  virtual void RemoveFieldObject(std::string_view name) = 0;
+  virtual void ForEachFieldObject(
+      wpi::function_ref<void(FieldObjectModel& model, std::string_view name)>
+          func) = 0;
+};
+
+void DisplayField2D(Field2DModel* model, const ImVec2& contentSize);
+void DisplayField2DSettings(Field2DModel* model);
+
+class Field2DView : public View {
+ public:
+  explicit Field2DView(Field2DModel* model) : m_model{model} {}
+
+  void Display() override;
+
+ private:
+  Field2DModel* m_model;
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/Log.h b/glass/src/lib/native/include/glass/other/Log.h
new file mode 100644
index 0000000..3d9c59b
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/Log.h
@@ -0,0 +1,44 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "glass/View.h"
+
+namespace glass {
+
+class LogData {
+  friend void DisplayLog(LogData*, bool);
+
+ public:
+  explicit LogData(size_t maxLines = 10000);
+
+  void Clear();
+  void Append(std::string_view msg);
+  const std::string& GetBuffer();
+
+ private:
+  size_t m_maxLines;
+  std::string m_buf;
+  std::vector<size_t> m_lineOffsets{0};
+};
+
+void DisplayLog(LogData* data, bool autoScroll);
+
+class LogView : public View {
+ public:
+  explicit LogView(LogData* data) : m_data{data} {}
+
+  void Display() override;
+
+ private:
+  LogData* m_data;
+  bool m_autoScroll{true};
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/Mechanism2D.h b/glass/src/lib/native/include/glass/other/Mechanism2D.h
new file mode 100644
index 0000000..7617e6f
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/Mechanism2D.h
@@ -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.
+
+#pragma once
+
+#include <frc/geometry/Rotation2d.h>
+#include <frc/geometry/Translation2d.h>
+#include <imgui.h>
+#include <wpi/function_ref.h>
+
+#include "glass/Model.h"
+#include "glass/View.h"
+
+namespace glass {
+
+class MechanismObjectModel;
+
+class MechanismObjectGroup {
+ public:
+  virtual const char* GetName() const = 0;
+  virtual void ForEachObject(
+      wpi::function_ref<void(MechanismObjectModel& model)> func) = 0;
+};
+
+class MechanismObjectModel : public MechanismObjectGroup {
+ public:
+  virtual const char* GetType() const = 0;
+  virtual ImU32 GetColor() const = 0;
+
+  // line accessors
+  virtual double GetWeight() const = 0;
+  virtual frc::Rotation2d GetAngle() const = 0;
+  virtual units::meter_t GetLength() const = 0;
+};
+
+class MechanismRootModel : public MechanismObjectGroup {
+ public:
+  virtual frc::Translation2d GetPosition() const = 0;
+};
+
+class Mechanism2DModel : public Model {
+ public:
+  virtual frc::Translation2d GetDimensions() const = 0;
+  virtual ImU32 GetBackgroundColor() const = 0;
+  virtual void ForEachRoot(
+      wpi::function_ref<void(MechanismRootModel& model)> func) = 0;
+};
+
+void DisplayMechanism2D(Mechanism2DModel* model, const ImVec2& contentSize);
+void DisplayMechanism2DSettings(Mechanism2DModel* model);
+
+class Mechanism2DView : public View {
+ public:
+  explicit Mechanism2DView(Mechanism2DModel* model) : m_model{model} {}
+
+  void Display() override;
+
+ private:
+  Mechanism2DModel* m_model;
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/PIDController.h b/glass/src/lib/native/include/glass/other/PIDController.h
new file mode 100644
index 0000000..ab0dcb3
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/PIDController.h
@@ -0,0 +1,26 @@
+// 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.
+
+#pragma once
+
+#include "glass/Model.h"
+
+namespace glass {
+class DataSource;
+class PIDControllerModel : public Model {
+ public:
+  virtual const char* GetName() const = 0;
+
+  virtual DataSource* GetPData() = 0;
+  virtual DataSource* GetIData() = 0;
+  virtual DataSource* GetDData() = 0;
+  virtual DataSource* GetSetpointData() = 0;
+
+  virtual void SetP(double value) = 0;
+  virtual void SetI(double value) = 0;
+  virtual void SetD(double value) = 0;
+  virtual void SetSetpoint(double value) = 0;
+};
+void DisplayPIDController(PIDControllerModel* m);
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/Plot.h b/glass/src/lib/native/include/glass/other/Plot.h
new file mode 100644
index 0000000..f7b196d
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/Plot.h
@@ -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.
+
+#pragma once
+
+#include <string_view>
+
+#include "glass/WindowManager.h"
+#include "glass/support/IniSaverBase.h"
+
+namespace glass {
+
+class PlotProvider : private WindowManager {
+ public:
+  explicit PlotProvider(std::string_view iniName);
+  ~PlotProvider() override;
+
+  void GlobalInit() override;
+
+  /**
+   * Pauses or unpauses all plots.
+   *
+   * @param paused true to pause, false to unpause
+   */
+  void SetPaused(bool paused) { m_paused = paused; }
+
+  /**
+   * Returns true if all plots are paused.
+   */
+  bool IsPaused() { return m_paused; }
+
+  void DisplayMenu() override;
+
+ private:
+  void DisplayWindows() override;
+
+  class IniSaver : public IniSaverBase {
+   public:
+    explicit IniSaver(std::string_view typeName, PlotProvider* provider,
+                      bool forSeries);
+
+    void* IniReadOpen(const char* name) override;
+    void IniReadLine(void* entry, const char* lineStr) override;
+    void IniWriteAll(ImGuiTextBuffer* out_buf) override;
+
+   private:
+    PlotProvider* m_provider;
+    bool m_forSeries;
+  };
+
+  IniSaver m_plotSaver;
+  IniSaver m_seriesSaver;
+  bool m_paused = false;
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/StringChooser.h b/glass/src/lib/native/include/glass/other/StringChooser.h
new file mode 100644
index 0000000..77f9ac2
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/StringChooser.h
@@ -0,0 +1,32 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <wpi/span.h>
+
+#include "glass/Model.h"
+
+namespace glass {
+
+class StringChooserModel : public Model {
+ public:
+  virtual const std::string& GetDefault() = 0;
+  virtual const std::string& GetSelected() = 0;
+  virtual const std::string& GetActive() = 0;
+  virtual const std::vector<std::string>& GetOptions() = 0;
+
+  virtual void SetDefault(std::string_view val) = 0;
+  virtual void SetSelected(std::string_view val) = 0;
+  virtual void SetActive(std::string_view val) = 0;
+  virtual void SetOptions(wpi::span<const std::string> val) = 0;
+};
+
+void DisplayStringChooser(StringChooserModel* model);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/Subsystem.h b/glass/src/lib/native/include/glass/other/Subsystem.h
new file mode 100644
index 0000000..db79db5
--- /dev/null
+++ b/glass/src/lib/native/include/glass/other/Subsystem.h
@@ -0,0 +1,18 @@
+// 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.
+
+#pragma once
+
+#include "glass/Model.h"
+
+namespace glass {
+class DataSource;
+class SubsystemModel : public Model {
+ public:
+  virtual const char* GetName() const = 0;
+  virtual const char* GetDefaultCommand() const = 0;
+  virtual const char* GetCurrentCommand() const = 0;
+};
+void DisplaySubsystem(SubsystemModel* m);
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h b/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h
new file mode 100644
index 0000000..3a35335
--- /dev/null
+++ b/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h
@@ -0,0 +1,96 @@
+// 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.
+
+#pragma once
+
+#include <imgui.h>
+
+namespace glass {
+
+class DataSource;
+
+/**
+ * DrawLEDs() configuration for 2D arrays.
+ */
+struct LEDConfig {
+  /**
+   * Whether the major order is serpentined (e.g. the first row goes left to
+   * right, the second row right to left, the third row left to right, and so
+   * on).
+   */
+  bool serpentine = false;
+
+  /**
+   * The input array order (row-major or column-major).
+   */
+  enum Order { RowMajor = 0, ColumnMajor } order = RowMajor;
+
+  /**
+   * The starting location of the array (0 location).
+   */
+  enum Start {
+    UpperLeft = 0,
+    LowerLeft,
+    UpperRight,
+    LowerRight
+  } start = UpperLeft;
+};
+
+/**
+ * Draw a 2D array of LEDs.
+ *
+ * Values are indices into colors array.  Positive values are filled (lit),
+ * negative values are unfilled (dark / border only).  The actual color index
+ * is the absolute value of the value - 1.  0 values are not drawn at all
+ * (an empty space is left).
+ *
+ * @param values values array
+ * @param numValues size of values array
+ * @param cols number of columns
+ * @param colors colors array
+ * @param size size of each LED (both horizontal and vertical);
+ *             if 0, defaults to 1/2 of font size
+ * @param spacing spacing between each LED (both horizontal and vertical);
+ *                if 0, defaults to 1/3 of font size
+ * @param config 2D array configuration
+ */
+void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors,
+              float size = 0.0f, float spacing = 0.0f,
+              const LEDConfig& config = LEDConfig{});
+
+/**
+ * Draw a 2D array of LEDs.
+ *
+ * Values are indices into colors array.  Positive values are filled (lit),
+ * negative values are unfilled (dark / border only).  The actual color index
+ * is the absolute value of the value - 1.  0 values are not drawn at all
+ * (an empty space is left).
+ *
+ * @param values values array
+ * @param sources sources array
+ * @param numValues size of values and sources arrays
+ * @param cols number of columns
+ * @param colors colors array
+ * @param size size of each LED (both horizontal and vertical);
+ *             if 0, defaults to 1/2 of font size
+ * @param spacing spacing between each LED (both horizontal and vertical);
+ *                if 0, defaults to 1/3 of font size
+ * @param config 2D array configuration
+ */
+void DrawLEDSources(const int* values, DataSource** sources, int numValues,
+                    int cols, const ImU32* colors, float size = 0.0f,
+                    float spacing = 0.0f,
+                    const LEDConfig& config = LEDConfig{});
+
+/**
+ * Delete button (X in circle), based on ImGui::CloseButton().
+ */
+bool DeleteButton(ImGuiID id, const ImVec2& pos);
+
+/**
+ * Create a small overlapping delete button for collapsing headers.
+ */
+bool HeaderDeleteButton(const char* label);
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/support/IniSaver.h b/glass/src/lib/native/include/glass/support/IniSaver.h
new file mode 100644
index 0000000..a3aa3d0
--- /dev/null
+++ b/glass/src/lib/native/include/glass/support/IniSaver.h
@@ -0,0 +1,44 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+
+#include <imgui.h>
+#include <wpi/DenseMap.h>
+
+#include "glass/support/IniSaverBase.h"
+
+namespace glass {
+
+template <typename Info>
+class IniSaver : public IniSaverBase {
+ public:
+  explicit IniSaver(std::string_view typeName,
+                    IniSaverBackend* backend = nullptr)
+      : IniSaverBase(typeName, backend) {}
+
+  // pass through useful functions to map
+  Info& operator[](int index) { return m_map[index]; }
+
+  auto begin() { return m_map.begin(); }
+  auto end() { return m_map.end(); }
+  auto find(int index) { return m_map.find(index); }
+
+  auto begin() const { return m_map.begin(); }
+  auto end() const { return m_map.end(); }
+  auto find(int index) const { return m_map.find(index); }
+
+ private:
+  void* IniReadOpen(const char* name) override;
+  void IniReadLine(void* entry, const char* lineStr) override;
+  void IniWriteAll(ImGuiTextBuffer* out_buf) override;
+
+  wpi::DenseMap<int, Info> m_map;
+};
+
+}  // namespace glass
+
+#include "IniSaver.inc"
diff --git a/glass/src/lib/native/include/glass/support/IniSaver.inc b/glass/src/lib/native/include/glass/support/IniSaver.inc
new file mode 100644
index 0000000..42efb85
--- /dev/null
+++ b/glass/src/lib/native/include/glass/support/IniSaver.inc
@@ -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.
+
+#pragma once
+
+#include <string_view>
+
+#include <wpi/StringExtras.h>
+
+#include "glass/support/IniSaver.h"
+
+namespace glass {
+
+template <typename Info>
+void* IniSaver<Info>::IniReadOpen(const char* name) {
+  if (auto num = wpi::parse_integer<int>(name, 10)) {
+    return &m_map[num.value()];
+  } else {
+    return nullptr;
+  }
+}
+
+template <typename Info>
+void IniSaver<Info>::IniReadLine(void* entry, const char* line) {
+  auto element = static_cast<Info*>(entry);
+  auto [name, value] = wpi::split(line, '=');
+  element->ReadIni(wpi::trim(name), wpi::trim(value));
+}
+
+template <typename Info>
+void IniSaver<Info>::IniWriteAll(ImGuiTextBuffer* out_buf) {
+  for (auto&& it : m_map) {
+    out_buf->appendf("[%s][%d]\n", GetTypeName(), it.first);
+    it.second.WriteIni(out_buf);
+    out_buf->append("\n");
+  }
+}
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/support/IniSaverBase.h b/glass/src/lib/native/include/glass/support/IniSaverBase.h
new file mode 100644
index 0000000..85ae1e3
--- /dev/null
+++ b/glass/src/lib/native/include/glass/support/IniSaverBase.h
@@ -0,0 +1,46 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <imgui.h>
+
+namespace glass {
+
+class IniSaverBase;
+
+class IniSaverBackend {
+ public:
+  virtual ~IniSaverBackend() = default;
+  virtual void Register(IniSaverBase* iniSaver) = 0;
+  virtual void Unregister(IniSaverBase* iniSaver) = 0;
+};
+
+class IniSaverBase {
+ public:
+  explicit IniSaverBase(std::string_view typeName,
+                        IniSaverBackend* backend = nullptr);
+  virtual ~IniSaverBase();
+
+  void Initialize() { m_backend->Register(this); }
+
+  const char* GetTypeName() const { return m_typeName.c_str(); }
+  IniSaverBackend* GetBackend() const { return m_backend; }
+
+  IniSaverBase(const IniSaverBase&) = delete;
+  IniSaverBase& operator=(const IniSaverBase&) = delete;
+
+  virtual void* IniReadOpen(const char* name) = 0;
+  virtual void IniReadLine(void* entry, const char* lineStr) = 0;
+  virtual void IniWriteAll(ImGuiTextBuffer* out_buf) = 0;
+
+ private:
+  std::string m_typeName;
+  IniSaverBackend* m_backend;
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/support/IniSaverInfo.h b/glass/src/lib/native/include/glass/support/IniSaverInfo.h
new file mode 100644
index 0000000..2014e9d
--- /dev/null
+++ b/glass/src/lib/native/include/glass/support/IniSaverInfo.h
@@ -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.
+
+#pragma once
+
+#include <string_view>
+
+#include <imgui.h>
+
+namespace glass {
+
+class NameInfo {
+ public:
+  NameInfo() { m_name[0] = '\0'; }
+
+  bool HasName() const { return m_name[0] != '\0'; }
+  void SetName(std::string_view name);
+  const char* GetName() const { return m_name; }
+  void GetName(char* buf, size_t size, const char* defaultName) const;
+  void GetName(char* buf, size_t size, const char* defaultName,
+               int index) const;
+  void GetName(char* buf, size_t size, const char* defaultName, int index,
+               int index2) const;
+  void GetLabel(char* buf, size_t size, const char* defaultName) const;
+  void GetLabel(char* buf, size_t size, const char* defaultName,
+                int index) const;
+  void GetLabel(char* buf, size_t size, const char* defaultName, int index,
+                int index2) const;
+
+  bool ReadIni(std::string_view name, std::string_view value);
+  void WriteIni(ImGuiTextBuffer* out);
+  void PushEditNameId(int index);
+  void PushEditNameId(const char* name);
+  bool PopupEditName(int index);
+  bool PopupEditName(const char* name);
+  bool InputTextName(const char* label_id, ImGuiInputTextFlags flags = 0);
+
+ private:
+  char m_name[64];
+};
+
+class OpenInfo {
+ public:
+  OpenInfo() = default;
+  explicit OpenInfo(bool open) : m_open(open) {}
+
+  bool IsOpen() const { return m_open; }
+  void SetOpen(bool open) { m_open = open; }
+  bool ReadIni(std::string_view name, std::string_view value);
+  void WriteIni(ImGuiTextBuffer* out);
+
+ private:
+  bool m_open = false;
+};
+
+class NameOpenInfo : public NameInfo, public OpenInfo {
+ public:
+  bool ReadIni(std::string_view name, std::string_view value);
+  void WriteIni(ImGuiTextBuffer* out);
+};
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/support/IniSaverString.h b/glass/src/lib/native/include/glass/support/IniSaverString.h
new file mode 100644
index 0000000..4134219
--- /dev/null
+++ b/glass/src/lib/native/include/glass/support/IniSaverString.h
@@ -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.
+
+#pragma once
+
+#include <string_view>
+#include <utility>
+
+#include <imgui.h>
+#include <wpi/StringMap.h>
+
+#include "glass/support/IniSaverBase.h"
+
+namespace glass {
+
+template <typename Info>
+class IniSaverString : public IniSaverBase {
+ public:
+  explicit IniSaverString(std::string_view typeName,
+                          IniSaverBackend* backend = nullptr)
+      : IniSaverBase(typeName, backend) {}
+
+  // pass through useful functions to map
+  Info& operator[](std::string_view key) { return m_map[key]; }
+
+  template <typename... ArgsTy>
+  auto try_emplace(std::string_view key, ArgsTy&&... args) {
+    return m_map.try_emplace(key, std::forward<ArgsTy>(args)...);
+  }
+
+  void erase(typename wpi::StringMap<Info>::iterator it) { m_map.erase(it); }
+  auto erase(std::string_view key) { return m_map.erase(key); }
+
+  auto begin() { return m_map.begin(); }
+  auto end() { return m_map.end(); }
+  auto find(std::string_view key) { return m_map.find(key); }
+
+  auto begin() const { return m_map.begin(); }
+  auto end() const { return m_map.end(); }
+  auto find(std::string_view key) const { return m_map.find(key); }
+
+  bool empty() const { return m_map.empty(); }
+
+ private:
+  void* IniReadOpen(const char* name) override;
+  void IniReadLine(void* entry, const char* lineStr) override;
+  void IniWriteAll(ImGuiTextBuffer* out_buf) override;
+
+  wpi::StringMap<Info> m_map;
+};
+
+}  // namespace glass
+
+#include "IniSaverString.inc"
diff --git a/glass/src/lib/native/include/glass/support/IniSaverString.inc b/glass/src/lib/native/include/glass/support/IniSaverString.inc
new file mode 100644
index 0000000..0d18d29
--- /dev/null
+++ b/glass/src/lib/native/include/glass/support/IniSaverString.inc
@@ -0,0 +1,36 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+
+#include <wpi/StringExtras.h>
+
+#include "glass/support/IniSaverString.h"
+
+namespace glass {
+
+template <typename Info>
+void* IniSaverString<Info>::IniReadOpen(const char* name) {
+  return &m_map[name];
+}
+
+template <typename Info>
+void IniSaverString<Info>::IniReadLine(void* entry, const char* line) {
+  auto element = static_cast<Info*>(entry);
+  auto [name, value] = wpi::split(line, '=');
+  element->ReadIni(wpi::trim(name), wpi::trim(value));
+}
+
+template <typename Info>
+void IniSaverString<Info>::IniWriteAll(ImGuiTextBuffer* out_buf) {
+  for (auto&& it : m_map) {
+    out_buf->appendf("[%s][%s]\n", GetTypeName(), it.getKey().data());
+    it.second.WriteIni(out_buf);
+    out_buf->append("\n");
+  }
+}
+
+}  // namespace glass
diff --git a/glass/src/lib/native/include/glass/support/IniSaverVector.h b/glass/src/lib/native/include/glass/support/IniSaverVector.h
new file mode 100644
index 0000000..e2e57ce
--- /dev/null
+++ b/glass/src/lib/native/include/glass/support/IniSaverVector.h
@@ -0,0 +1,31 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+#include <vector>
+
+#include <imgui.h>
+
+#include "glass/support/IniSaverBase.h"
+
+namespace glass {
+
+template <typename Info>
+class IniSaverVector : public std::vector<Info>, public IniSaverBase {
+ public:
+  explicit IniSaverVector(std::string_view typeName,
+                          IniSaverBackend* backend = nullptr)
+      : IniSaverBase(typeName, backend) {}
+
+ private:
+  void* IniReadOpen(const char* name) override;
+  void IniReadLine(void* entry, const char* lineStr) override;
+  void IniWriteAll(ImGuiTextBuffer* out_buf) override;
+};
+
+}  // namespace glass
+
+#include "IniSaverVector.inc"
diff --git a/glass/src/lib/native/include/glass/support/IniSaverVector.inc b/glass/src/lib/native/include/glass/support/IniSaverVector.inc
new file mode 100644
index 0000000..a86b116
--- /dev/null
+++ b/glass/src/lib/native/include/glass/support/IniSaverVector.inc
@@ -0,0 +1,43 @@
+// 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.
+
+#pragma once
+
+#include <string_view>
+
+#include <wpi/StringExtras.h>
+
+#include "glass/support/IniSaverVector.h"
+
+namespace glass {
+
+template <typename Info>
+void* IniSaverVector<Info>::IniReadOpen(const char* name) {
+  if (auto num = wpi::parse_integer<unsigned int>(name, 10)) {
+    if (num.value() >= this->size()) {
+      this->resize(num.value() + 1);
+    }
+    return &(*this)[num.value()];
+  } else {
+    return nullptr;
+  }
+}
+
+template <typename Info>
+void IniSaverVector<Info>::IniReadLine(void* entry, const char* line) {
+  auto element = static_cast<Info*>(entry);
+  auto [name, value] = wpi::split(line, '=');
+  element->ReadIni(wpi::trim(name), wpi::trim(value));
+}
+
+template <typename Info>
+void IniSaverVector<Info>::IniWriteAll(ImGuiTextBuffer* out_buf) {
+  for (size_t i = 0; i < this->size(); ++i) {
+    out_buf->appendf("[%s][%d]\n", GetTypeName(), static_cast<int>(i));
+    (*this)[i].WriteIni(out_buf);
+    out_buf->append("\n");
+  }
+}
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/cpp/NTCommandScheduler.cpp b/glass/src/libnt/native/cpp/NTCommandScheduler.cpp
new file mode 100644
index 0000000..ccc6412
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTCommandScheduler.cpp
@@ -0,0 +1,58 @@
+// 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/networktables/NTCommandScheduler.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTCommandSchedulerModel::NTCommandSchedulerModel(std::string_view path)
+    : NTCommandSchedulerModel(nt::GetDefaultInstance(), path) {}
+
+NTCommandSchedulerModel::NTCommandSchedulerModel(NT_Inst instance,
+                                                 std::string_view path)
+    : m_nt(instance),
+      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
+      m_commands(m_nt.GetEntry(fmt::format("{}/Names", path))),
+      m_ids(m_nt.GetEntry(fmt::format("{}/Ids", path))),
+      m_cancel(m_nt.GetEntry(fmt::format("{}/Cancel", path))),
+      m_nameValue(wpi::rsplit(path, '/').second) {
+  m_nt.AddListener(m_name);
+  m_nt.AddListener(m_commands);
+  m_nt.AddListener(m_ids);
+  m_nt.AddListener(m_cancel);
+}
+
+void NTCommandSchedulerModel::CancelCommand(size_t index) {
+  if (index < m_idsValue.size()) {
+    nt::SetEntryValue(
+        m_cancel, nt::NetworkTableValue::MakeDoubleArray({m_idsValue[index]}));
+  }
+}
+
+void NTCommandSchedulerModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_name) {
+      if (event.value && event.value->IsString()) {
+        m_nameValue = event.value->GetString();
+      }
+    } else if (event.entry == m_commands) {
+      if (event.value && event.value->IsStringArray()) {
+        auto arr = event.value->GetStringArray();
+        m_commandsValue.assign(arr.begin(), arr.end());
+      }
+    } else if (event.entry == m_ids) {
+      if (event.value && event.value->IsDoubleArray()) {
+        auto arr = event.value->GetDoubleArray();
+        m_idsValue.assign(arr.begin(), arr.end());
+      }
+    }
+  }
+}
+
+bool NTCommandSchedulerModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_commands) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTCommandSelector.cpp b/glass/src/libnt/native/cpp/NTCommandSelector.cpp
new file mode 100644
index 0000000..efcbac2
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTCommandSelector.cpp
@@ -0,0 +1,47 @@
+// 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/networktables/NTCommandSelector.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTCommandSelectorModel::NTCommandSelectorModel(std::string_view path)
+    : NTCommandSelectorModel(nt::GetDefaultInstance(), path) {}
+
+NTCommandSelectorModel::NTCommandSelectorModel(NT_Inst instance,
+                                               std::string_view path)
+    : m_nt(instance),
+      m_running(m_nt.GetEntry(fmt::format("{}/running", path))),
+      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
+      m_runningData(fmt::format("NTCmd:{}", path)),
+      m_nameValue(wpi::rsplit(path, '/').second) {
+  m_runningData.SetDigital(true);
+  m_nt.AddListener(m_running);
+  m_nt.AddListener(m_name);
+}
+
+void NTCommandSelectorModel::SetRunning(bool run) {
+  nt::SetEntryValue(m_running, nt::NetworkTableValue::MakeBoolean(run));
+}
+
+void NTCommandSelectorModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_running) {
+      if (event.value && event.value->IsBoolean()) {
+        m_runningData.SetValue(event.value->GetBoolean());
+      }
+    } else if (event.entry == m_name) {
+      if (event.value && event.value->IsString()) {
+        m_nameValue = event.value->GetString();
+      }
+    }
+  }
+}
+
+bool NTCommandSelectorModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_running) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp b/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp
new file mode 100644
index 0000000..b44ea07
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp
@@ -0,0 +1,66 @@
+// 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/networktables/NTDifferentialDrive.h"
+
+#include <fmt/format.h>
+#include <imgui.h>
+#include <wpi/MathExtras.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTDifferentialDriveModel::NTDifferentialDriveModel(std::string_view path)
+    : NTDifferentialDriveModel(nt::GetDefaultInstance(), path) {}
+
+NTDifferentialDriveModel::NTDifferentialDriveModel(NT_Inst instance,
+                                                   std::string_view path)
+    : m_nt(instance),
+      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
+      m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))),
+      m_lPercent(m_nt.GetEntry(fmt::format("{}/Left Motor Speed", path))),
+      m_rPercent(m_nt.GetEntry(fmt::format("{}/Right Motor Speed", path))),
+      m_nameValue(wpi::rsplit(path, '/').second),
+      m_lPercentData(fmt::format("NTDiffDriveL:{}", path)),
+      m_rPercentData(fmt::format("NTDiffDriveR:{}", path)) {
+  m_nt.AddListener(m_name);
+  m_nt.AddListener(m_controllable);
+  m_nt.AddListener(m_lPercent);
+  m_nt.AddListener(m_rPercent);
+
+  m_wheels.emplace_back("L % Output", &m_lPercentData, [this](auto value) {
+    nt::SetEntryValue(m_lPercent, nt::NetworkTableValue::MakeDouble(value));
+  });
+
+  m_wheels.emplace_back("R % Output", &m_rPercentData, [this](auto value) {
+    nt::SetEntryValue(m_rPercent, nt::NetworkTableValue::MakeDouble(value));
+  });
+}
+
+void NTDifferentialDriveModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_name && event.value && event.value->IsString()) {
+      m_nameValue = event.value->GetString();
+    } else if (event.entry == m_lPercent && event.value &&
+               event.value->IsDouble()) {
+      m_lPercentData.SetValue(event.value->GetDouble());
+    } else if (event.entry == m_rPercent && event.value &&
+               event.value->IsDouble()) {
+      m_rPercentData.SetValue(event.value->GetDouble());
+    } else if (event.entry == m_controllable && event.value &&
+               event.value->IsBoolean()) {
+      m_controllableValue = event.value->GetBoolean();
+    }
+  }
+
+  double l = m_lPercentData.GetValue();
+  double r = m_rPercentData.GetValue();
+
+  m_speedVector = ImVec2(0.0, -(l + r) / 2.0);
+  m_rotation = (l - r) / 2.0;
+}
+
+bool NTDifferentialDriveModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_lPercent) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTDigitalInput.cpp b/glass/src/libnt/native/cpp/NTDigitalInput.cpp
new file mode 100644
index 0000000..5de6c29
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTDigitalInput.cpp
@@ -0,0 +1,43 @@
+// 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/networktables/NTDigitalInput.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTDigitalInputModel::NTDigitalInputModel(std::string_view path)
+    : NTDigitalInputModel{nt::GetDefaultInstance(), path} {}
+
+NTDigitalInputModel::NTDigitalInputModel(NT_Inst inst, std::string_view path)
+    : m_nt{inst},
+      m_value{m_nt.GetEntry(fmt::format("{}/Value", path))},
+      m_name{m_nt.GetEntry(fmt::format("{}/.name", path))},
+      m_valueData{fmt::format("NT_DIn:{}", path)},
+      m_nameValue{wpi::rsplit(path, '/').second} {
+  m_nt.AddListener(m_value);
+  m_nt.AddListener(m_name);
+
+  m_valueData.SetDigital(true);
+}
+
+void NTDigitalInputModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_value) {
+      if (event.value && event.value->IsBoolean()) {
+        m_valueData.SetValue(event.value->GetBoolean());
+      }
+    } else if (event.entry == m_name) {
+      if (event.value && event.value->IsString()) {
+        m_nameValue = event.value->GetString();
+      }
+    }
+  }
+}
+
+bool NTDigitalInputModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTDigitalOutput.cpp b/glass/src/libnt/native/cpp/NTDigitalOutput.cpp
new file mode 100644
index 0000000..a09d424
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTDigitalOutput.cpp
@@ -0,0 +1,51 @@
+// 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/networktables/NTDigitalOutput.h"
+
+#include <fmt/format.h>
+
+using namespace glass;
+
+NTDigitalOutputModel::NTDigitalOutputModel(std::string_view path)
+    : NTDigitalOutputModel{nt::GetDefaultInstance(), path} {}
+
+NTDigitalOutputModel::NTDigitalOutputModel(NT_Inst inst, std::string_view path)
+    : m_nt{inst},
+      m_value{m_nt.GetEntry(fmt::format("{}/Value", path))},
+      m_name{m_nt.GetEntry(fmt::format("{}/.name", path))},
+      m_controllable{m_nt.GetEntry(fmt::format("{}/.controllable", path))},
+      m_valueData{fmt::format("NT_DOut:{}", path)} {
+  m_nt.AddListener(m_value);
+  m_nt.AddListener(m_name);
+  m_nt.AddListener(m_controllable);
+
+  m_valueData.SetDigital(true);
+}
+
+void NTDigitalOutputModel::SetValue(bool val) {
+  nt::SetEntryValue(m_value, nt::Value::MakeBoolean(val));
+}
+
+void NTDigitalOutputModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_value) {
+      if (event.value && event.value->IsBoolean()) {
+        m_valueData.SetValue(event.value->GetBoolean());
+      }
+    } else if (event.entry == m_name) {
+      if (event.value && event.value->IsString()) {
+        m_nameValue = event.value->GetString();
+      }
+    } else if (event.entry == m_controllable) {
+      if (event.value && event.value->IsBoolean()) {
+        m_controllableValue = event.value->GetBoolean();
+      }
+    }
+  }
+}
+
+bool NTDigitalOutputModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTFMS.cpp b/glass/src/libnt/native/cpp/NTFMS.cpp
new file mode 100644
index 0000000..84c1ce7
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTFMS.cpp
@@ -0,0 +1,91 @@
+// 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/networktables/NTFMS.h"
+
+#include <stdint.h>
+
+#include <fmt/format.h>
+#include <wpi/SmallVector.h>
+#include <wpi/timestamp.h>
+
+using namespace glass;
+
+NTFMSModel::NTFMSModel(std::string_view path)
+    : NTFMSModel{nt::GetDefaultInstance(), path} {}
+
+NTFMSModel::NTFMSModel(NT_Inst inst, std::string_view path)
+    : m_nt{inst},
+      m_gameSpecificMessage{
+          m_nt.GetEntry(fmt::format("{}/GameSpecificMessage", path))},
+      m_alliance{m_nt.GetEntry(fmt::format("{}/IsRedAlliance", path))},
+      m_station{m_nt.GetEntry(fmt::format("{}/StationNumber", path))},
+      m_controlWord{m_nt.GetEntry(fmt::format("{}/FMSControlData", path))},
+      m_fmsAttached{fmt::format("NT_FMS:FMSAttached:{}", path)},
+      m_dsAttached{fmt::format("NT_FMS:DSAttached:{}", path)},
+      m_allianceStationId{fmt::format("NT_FMS:AllianceStationID:{}", path)},
+      m_estop{fmt::format("NT_FMS:EStop:{}", path)},
+      m_enabled{fmt::format("NT_FMS:RobotEnabled:{}", path)},
+      m_test{fmt::format("NT_FMS:TestMode:{}", path)},
+      m_autonomous{fmt::format("NT_FMS:AutonomousMode:{}", path)} {
+  m_nt.AddListener(m_alliance);
+  m_nt.AddListener(m_station);
+  m_nt.AddListener(m_controlWord);
+
+  m_fmsAttached.SetDigital(true);
+  m_dsAttached.SetDigital(true);
+  m_estop.SetDigital(true);
+  m_enabled.SetDigital(true);
+  m_test.SetDigital(true);
+  m_autonomous.SetDigital(true);
+}
+
+std::string_view NTFMSModel::GetGameSpecificMessage(
+    wpi::SmallVectorImpl<char>& buf) {
+  buf.clear();
+  auto value = nt::GetEntryValue(m_gameSpecificMessage);
+  if (value && value->IsString()) {
+    auto str = value->GetString();
+    buf.append(str.begin(), str.end());
+  }
+  return std::string_view{buf.data(), buf.size()};
+}
+
+void NTFMSModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_alliance) {
+      if (event.value && event.value->IsBoolean()) {
+        int allianceStationId = m_allianceStationId.GetValue();
+        allianceStationId %= 3;
+        // true if red
+        allianceStationId += 3 * (event.value->GetBoolean() ? 0 : 1);
+        m_allianceStationId.SetValue(allianceStationId);
+      }
+    } else if (event.entry == m_station) {
+      if (event.value && event.value->IsDouble()) {
+        int allianceStationId = m_allianceStationId.GetValue();
+        bool isRed = (allianceStationId < 3);
+        // the NT value is 1-indexed
+        m_allianceStationId.SetValue(event.value->GetDouble() - 1 +
+                                     3 * (isRed ? 0 : 1));
+      }
+    } else if (event.entry == m_controlWord) {
+      if (event.value && event.value->IsDouble()) {
+        uint32_t controlWord = event.value->GetDouble();
+        // See HAL_ControlWord definition
+        auto time = wpi::Now();
+        m_enabled.SetValue(((controlWord & 0x01) != 0) ? 1 : 0, time);
+        m_autonomous.SetValue(((controlWord & 0x02) != 0) ? 1 : 0, time);
+        m_test.SetValue(((controlWord & 0x04) != 0) ? 1 : 0, time);
+        m_estop.SetValue(((controlWord & 0x08) != 0) ? 1 : 0, time);
+        m_fmsAttached.SetValue(((controlWord & 0x10) != 0) ? 1 : 0, time);
+        m_dsAttached.SetValue(((controlWord & 0x20) != 0) ? 1 : 0, time);
+      }
+    }
+  }
+}
+
+bool NTFMSModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_controlWord) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTField2D.cpp b/glass/src/libnt/native/cpp/NTField2D.cpp
new file mode 100644
index 0000000..47fa9a7
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTField2D.cpp
@@ -0,0 +1,255 @@
+// 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/networktables/NTField2D.h"
+
+#include <algorithm>
+#include <vector>
+
+#include <fmt/format.h>
+#include <ntcore_cpp.h>
+#include <wpi/Endian.h>
+#include <wpi/MathExtras.h>
+#include <wpi/SmallVector.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+class NTField2DModel::ObjectModel : public FieldObjectModel {
+ public:
+  ObjectModel(std::string_view name, NT_Entry entry)
+      : m_name{name}, m_entry{entry} {}
+
+  const char* GetName() const override { return m_name.c_str(); }
+  NT_Entry GetEntry() const { return m_entry; }
+
+  void NTUpdate(const nt::Value& value);
+
+  void Update() override {
+    if (auto value = nt::GetEntryValue(m_entry)) {
+      NTUpdate(*value);
+    }
+  }
+  bool Exists() override { return nt::GetEntryType(m_entry) != NT_UNASSIGNED; }
+  bool IsReadOnly() override { return false; }
+
+  wpi::span<const frc::Pose2d> GetPoses() override { return m_poses; }
+  void SetPoses(wpi::span<const frc::Pose2d> poses) override;
+  void SetPose(size_t i, frc::Pose2d pose) override;
+  void SetPosition(size_t i, frc::Translation2d pos) override;
+  void SetRotation(size_t i, frc::Rotation2d rot) override;
+
+ private:
+  void UpdateNT();
+
+  std::string m_name;
+  NT_Entry m_entry;
+
+  std::vector<frc::Pose2d> m_poses;
+};
+
+void NTField2DModel::ObjectModel::NTUpdate(const nt::Value& value) {
+  if (value.IsDoubleArray()) {
+    auto arr = value.GetDoubleArray();
+    auto size = arr.size();
+    if ((size % 3) != 0) {
+      return;
+    }
+    m_poses.resize(size / 3);
+    for (size_t i = 0; i < size / 3; ++i) {
+      m_poses[i] = frc::Pose2d{
+          units::meter_t{arr[i * 3 + 0]}, units::meter_t{arr[i * 3 + 1]},
+          frc::Rotation2d{units::degree_t{arr[i * 3 + 2]}}};
+    }
+  } else if (value.IsRaw()) {
+    // treat it simply as an array of doubles
+    std::string_view data = value.GetRaw();
+
+    // must be triples of doubles
+    auto size = data.size();
+    if ((size % (3 * 8)) != 0) {
+      return;
+    }
+    m_poses.resize(size / (3 * 8));
+    const char* p = data.data();
+    for (size_t i = 0; i < size / (3 * 8); ++i) {
+      double x = wpi::BitsToDouble(
+          wpi::support::endian::readNext<uint64_t, wpi::support::big,
+                                         wpi::support::unaligned>(p));
+      double y = wpi::BitsToDouble(
+          wpi::support::endian::readNext<uint64_t, wpi::support::big,
+                                         wpi::support::unaligned>(p));
+      double rot = wpi::BitsToDouble(
+          wpi::support::endian::readNext<uint64_t, wpi::support::big,
+                                         wpi::support::unaligned>(p));
+      m_poses[i] = frc::Pose2d{units::meter_t{x}, units::meter_t{y},
+                               frc::Rotation2d{units::degree_t{rot}}};
+    }
+  }
+}
+
+void NTField2DModel::ObjectModel::UpdateNT() {
+  if (m_poses.size() < (255 / 3)) {
+    wpi::SmallVector<double, 9> arr;
+    for (auto&& pose : m_poses) {
+      auto& translation = pose.Translation();
+      arr.push_back(translation.X().value());
+      arr.push_back(translation.Y().value());
+      arr.push_back(pose.Rotation().Degrees().value());
+    }
+    nt::SetEntryTypeValue(m_entry, nt::Value::MakeDoubleArray(arr));
+  } else {
+    // send as raw array of doubles if too big for NT array
+    std::vector<char> arr;
+    arr.resize(m_poses.size() * 3 * 8);
+    char* p = arr.data();
+    for (auto&& pose : m_poses) {
+      auto& translation = pose.Translation();
+      wpi::support::endian::write64be(
+          p, wpi::DoubleToBits(translation.X().value()));
+      p += 8;
+      wpi::support::endian::write64be(
+          p, wpi::DoubleToBits(translation.Y().value()));
+      p += 8;
+      wpi::support::endian::write64be(
+          p, wpi::DoubleToBits(pose.Rotation().Degrees().value()));
+      p += 8;
+    }
+    nt::SetEntryTypeValue(m_entry,
+                          nt::Value::MakeRaw({arr.data(), arr.size()}));
+  }
+}
+
+void NTField2DModel::ObjectModel::SetPoses(wpi::span<const frc::Pose2d> poses) {
+  m_poses.assign(poses.begin(), poses.end());
+  UpdateNT();
+}
+
+void NTField2DModel::ObjectModel::SetPose(size_t i, frc::Pose2d pose) {
+  if (i < m_poses.size()) {
+    m_poses[i] = pose;
+    UpdateNT();
+  }
+}
+
+void NTField2DModel::ObjectModel::SetPosition(size_t i,
+                                              frc::Translation2d pos) {
+  if (i < m_poses.size()) {
+    m_poses[i] = frc::Pose2d{pos, m_poses[i].Rotation()};
+    UpdateNT();
+  }
+}
+
+void NTField2DModel::ObjectModel::SetRotation(size_t i, frc::Rotation2d rot) {
+  if (i < m_poses.size()) {
+    m_poses[i] = frc::Pose2d{m_poses[i].Translation(), rot};
+    UpdateNT();
+  }
+}
+
+NTField2DModel::NTField2DModel(std::string_view path)
+    : NTField2DModel{nt::GetDefaultInstance(), path} {}
+
+NTField2DModel::NTField2DModel(NT_Inst inst, std::string_view path)
+    : m_nt{inst},
+      m_path{fmt::format("{}/", path)},
+      m_name{m_nt.GetEntry(fmt::format("{}/.name", path))} {
+  m_nt.AddListener(m_path, NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE |
+                               NT_NOTIFY_UPDATE | NT_NOTIFY_IMMEDIATE);
+}
+
+NTField2DModel::~NTField2DModel() = default;
+
+void NTField2DModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    // .name
+    if (event.entry == m_name) {
+      if (event.value && event.value->IsString()) {
+        m_nameValue = event.value->GetString();
+      }
+      continue;
+    }
+
+    // common case: update of existing entry; search by entry
+    if (event.flags & NT_NOTIFY_UPDATE) {
+      auto it = std::find_if(
+          m_objects.begin(), m_objects.end(),
+          [&](const auto& e) { return e->GetEntry() == event.entry; });
+      if (it != m_objects.end()) {
+        (*it)->NTUpdate(*event.value);
+        continue;
+      }
+    }
+
+    // handle create/delete
+    std::string_view name = event.name;
+    if (wpi::starts_with(name, m_path)) {
+      name.remove_prefix(m_path.size());
+      if (name.empty() || name[0] == '.') {
+        continue;
+      }
+      auto [it, match] = Find(event.name);
+      if (event.flags & NT_NOTIFY_DELETE) {
+        if (match) {
+          m_objects.erase(it);
+        }
+        continue;
+      } else if (event.flags & NT_NOTIFY_NEW) {
+        if (!match) {
+          it = m_objects.emplace(
+              it, std::make_unique<ObjectModel>(event.name, event.entry));
+        }
+      } else if (!match) {
+        continue;
+      }
+      if (event.flags & (NT_NOTIFY_NEW | NT_NOTIFY_UPDATE)) {
+        (*it)->NTUpdate(*event.value);
+      }
+    }
+  }
+}
+
+bool NTField2DModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_name) != NT_UNASSIGNED;
+}
+
+bool NTField2DModel::IsReadOnly() {
+  return false;
+}
+
+FieldObjectModel* NTField2DModel::AddFieldObject(std::string_view name) {
+  auto fullName = fmt::format("{}{}", m_path, name);
+  auto [it, match] = Find(fullName);
+  if (!match) {
+    it = m_objects.emplace(
+        it, std::make_unique<ObjectModel>(fullName, m_nt.GetEntry(fullName)));
+  }
+  return it->get();
+}
+
+void NTField2DModel::RemoveFieldObject(std::string_view name) {
+  auto [it, match] = Find(fmt::format("{}{}", m_path, name));
+  if (match) {
+    nt::DeleteEntry((*it)->GetEntry());
+    m_objects.erase(it);
+  }
+}
+
+void NTField2DModel::ForEachFieldObject(
+    wpi::function_ref<void(FieldObjectModel& model, std::string_view name)>
+        func) {
+  for (auto&& obj : m_objects) {
+    if (obj->Exists()) {
+      func(*obj, wpi::drop_front(obj->GetName(), m_path.size()));
+    }
+  }
+}
+
+std::pair<NTField2DModel::Objects::iterator, bool> NTField2DModel::Find(
+    std::string_view fullName) {
+  auto it = std::lower_bound(
+      m_objects.begin(), m_objects.end(), fullName,
+      [](const auto& e, std::string_view name) { return e->GetName() < name; });
+  return {it, it != m_objects.end() && (*it)->GetName() == fullName};
+}
diff --git a/glass/src/libnt/native/cpp/NTGyro.cpp b/glass/src/libnt/native/cpp/NTGyro.cpp
new file mode 100644
index 0000000..7651d2c
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTGyro.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/networktables/NTGyro.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTGyroModel::NTGyroModel(std::string_view path)
+    : NTGyroModel(nt::GetDefaultInstance(), path) {}
+
+NTGyroModel::NTGyroModel(NT_Inst instance, std::string_view path)
+    : m_nt(instance),
+      m_angle(m_nt.GetEntry(fmt::format("{}/Value", path))),
+      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
+      m_angleData(fmt::format("NT_Gyro:{}", path)),
+      m_nameValue(wpi::rsplit(path, '/').second) {
+  m_nt.AddListener(m_angle);
+  m_nt.AddListener(m_name);
+}
+
+void NTGyroModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_angle) {
+      if (event.value && event.value->IsDouble()) {
+        m_angleData.SetValue(event.value->GetDouble());
+      }
+    } else if (event.entry == m_name) {
+      if (event.value && event.value->IsString()) {
+        m_nameValue = event.value->GetString();
+      }
+    }
+  }
+}
+
+bool NTGyroModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_angle) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTMecanumDrive.cpp b/glass/src/libnt/native/cpp/NTMecanumDrive.cpp
new file mode 100644
index 0000000..28c0a67
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTMecanumDrive.cpp
@@ -0,0 +1,92 @@
+// 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/networktables/NTMecanumDrive.h"
+
+#include <fmt/format.h>
+#include <imgui.h>
+#include <wpi/MathExtras.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTMecanumDriveModel::NTMecanumDriveModel(std::string_view path)
+    : NTMecanumDriveModel(nt::GetDefaultInstance(), path) {}
+
+NTMecanumDriveModel::NTMecanumDriveModel(NT_Inst instance,
+                                         std::string_view path)
+    : m_nt(instance),
+      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
+      m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))),
+      m_flPercent(
+          m_nt.GetEntry(fmt::format("{}/Front Left Motor Speed", path))),
+      m_frPercent(
+          m_nt.GetEntry(fmt::format("{}/Front Right Motor Speed", path))),
+      m_rlPercent(m_nt.GetEntry(fmt::format("{}/Rear Left Motor Speed", path))),
+      m_rrPercent(
+          m_nt.GetEntry(fmt::format("{}/Rear Right Motor Speed", path))),
+      m_nameValue(wpi::rsplit(path, '/').second),
+      m_flPercentData(fmt::format("NTMcnmDriveFL:{}", path)),
+      m_frPercentData(fmt::format("NTMcnmDriveFR:{}", path)),
+      m_rlPercentData(fmt::format("NTMcnmDriveRL:{}", path)),
+      m_rrPercentData(fmt::format("NTMcnmDriveRR:{}", path)) {
+  m_nt.AddListener(m_name);
+  m_nt.AddListener(m_controllable);
+  m_nt.AddListener(m_flPercent);
+  m_nt.AddListener(m_frPercent);
+  m_nt.AddListener(m_rlPercent);
+  m_nt.AddListener(m_rrPercent);
+
+  m_wheels.emplace_back("FL % Output", &m_flPercentData, [this](auto value) {
+    nt::SetEntryValue(m_flPercent, nt::NetworkTableValue::MakeDouble(value));
+  });
+
+  m_wheels.emplace_back("FR % Output", &m_frPercentData, [this](auto value) {
+    nt::SetEntryValue(m_frPercent, nt::NetworkTableValue::MakeDouble(value));
+  });
+
+  m_wheels.emplace_back("RL % Output", &m_rlPercentData, [this](auto value) {
+    nt::SetEntryValue(m_rlPercent, nt::NetworkTableValue::MakeDouble(value));
+  });
+
+  m_wheels.emplace_back("RR % Output", &m_rrPercentData, [this](auto value) {
+    nt::SetEntryValue(m_rrPercent, nt::NetworkTableValue::MakeDouble(value));
+  });
+}
+
+void NTMecanumDriveModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_name && event.value && event.value->IsString()) {
+      m_nameValue = event.value->GetString();
+    } else if (event.entry == m_flPercent && event.value &&
+               event.value->IsDouble()) {
+      m_flPercentData.SetValue(event.value->GetDouble());
+    } else if (event.entry == m_frPercent && event.value &&
+               event.value->IsDouble()) {
+      m_frPercentData.SetValue(event.value->GetDouble());
+    } else if (event.entry == m_rlPercent && event.value &&
+               event.value->IsDouble()) {
+      m_rlPercentData.SetValue(event.value->GetDouble());
+    } else if (event.entry == m_rrPercent && event.value &&
+               event.value->IsDouble()) {
+      m_rrPercentData.SetValue(event.value->GetDouble());
+    } else if (event.entry == m_controllable && event.value &&
+               event.value->IsBoolean()) {
+      m_controllableValue = event.value->GetBoolean();
+    }
+  }
+
+  double fl = m_flPercentData.GetValue();
+  double fr = m_frPercentData.GetValue();
+  double rl = m_rlPercentData.GetValue();
+  double rr = m_rrPercentData.GetValue();
+
+  m_speedVector =
+      ImVec2((fl - fr - rl + rr) / 4.0f, -(fl + fr + rl + rr) / 4.0f);
+  m_rotation = -(-fl + fr - rl + rr) / 4;
+}
+
+bool NTMecanumDriveModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_flPercent) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTMechanism2D.cpp b/glass/src/libnt/native/cpp/NTMechanism2D.cpp
new file mode 100644
index 0000000..9c73af2
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTMechanism2D.cpp
@@ -0,0 +1,302 @@
+// 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/networktables/NTMechanism2D.h"
+
+#include <algorithm>
+#include <string_view>
+#include <vector>
+
+#include <fmt/format.h>
+#include <imgui.h>
+#include <ntcore_cpp.h>
+#include <wpi/StringExtras.h>
+
+#include "glass/other/Mechanism2D.h"
+
+using namespace glass;
+
+// Convert "#RRGGBB" hex color to ImU32 color
+static void ConvertColor(std::string_view in, ImU32* out) {
+  if (in.size() != 7 || in[0] != '#') {
+    return;
+  }
+  if (auto v = wpi::parse_integer<ImU32>(wpi::drop_front(in), 16)) {
+    ImU32 val = v.value();
+    *out = IM_COL32((val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff, 255);
+  }
+}
+
+namespace {
+
+class NTMechanismObjectModel;
+
+class NTMechanismGroupImpl final {
+ public:
+  NTMechanismGroupImpl(NT_Inst inst, std::string_view path,
+                       std::string_view name)
+      : m_inst{inst}, m_path{path}, m_name{name} {}
+
+  const char* GetName() const { return m_name.c_str(); }
+  void ForEachObject(wpi::function_ref<void(MechanismObjectModel& model)> func);
+  void NTUpdate(const nt::EntryNotification& event, std::string_view name);
+
+ protected:
+  NT_Inst m_inst;
+  std::string m_path;
+  std::string m_name;
+  std::vector<std::unique_ptr<NTMechanismObjectModel>> m_objects;
+};
+
+class NTMechanismObjectModel final : public MechanismObjectModel {
+ public:
+  NTMechanismObjectModel(NT_Inst inst, std::string_view path,
+                         std::string_view name)
+      : m_group{inst, path, name},
+        m_type{nt::GetEntry(inst, fmt::format("{}/.type", path))},
+        m_color{nt::GetEntry(inst, fmt::format("{}/color", path))},
+        m_weight{nt::GetEntry(inst, fmt::format("{}/weight", path))},
+        m_angle{nt::GetEntry(inst, fmt::format("{}/angle", path))},
+        m_length{nt::GetEntry(inst, fmt::format("{}/length", path))} {}
+
+  const char* GetName() const final { return m_group.GetName(); }
+  void ForEachObject(
+      wpi::function_ref<void(MechanismObjectModel& model)> func) final {
+    m_group.ForEachObject(func);
+  }
+
+  const char* GetType() const final { return m_typeValue.c_str(); }
+  ImU32 GetColor() const final { return m_colorValue; }
+  double GetWeight() const final { return m_weightValue; }
+  frc::Rotation2d GetAngle() const final { return m_angleValue; }
+  units::meter_t GetLength() const final { return m_lengthValue; }
+
+  bool NTUpdate(const nt::EntryNotification& event, std::string_view childName);
+
+ private:
+  NTMechanismGroupImpl m_group;
+
+  NT_Entry m_type;
+  NT_Entry m_color;
+  NT_Entry m_weight;
+  NT_Entry m_angle;
+  NT_Entry m_length;
+
+  std::string m_typeValue;
+  ImU32 m_colorValue = IM_COL32_WHITE;
+  double m_weightValue = 1.0;
+  frc::Rotation2d m_angleValue;
+  units::meter_t m_lengthValue = 0.0_m;
+};
+
+}  // namespace
+
+void NTMechanismGroupImpl::ForEachObject(
+    wpi::function_ref<void(MechanismObjectModel& model)> func) {
+  for (auto&& obj : m_objects) {
+    func(*obj);
+  }
+}
+
+void NTMechanismGroupImpl::NTUpdate(const nt::EntryNotification& event,
+                                    std::string_view name) {
+  if (name.empty()) {
+    return;
+  }
+  std::string_view childName;
+  std::tie(name, childName) = wpi::split(name, '/');
+  if (childName.empty()) {
+    return;
+  }
+
+  auto it = std::lower_bound(
+      m_objects.begin(), m_objects.end(), name,
+      [](const auto& e, std::string_view name) { return e->GetName() < name; });
+  bool match = it != m_objects.end() && (*it)->GetName() == name;
+
+  if (event.flags & NT_NOTIFY_NEW) {
+    if (!match) {
+      it = m_objects.emplace(
+          it, std::make_unique<NTMechanismObjectModel>(
+                  m_inst, fmt::format("{}/{}", m_path, name), name));
+      match = true;
+    }
+  }
+  if (match) {
+    if ((*it)->NTUpdate(event, childName)) {
+      m_objects.erase(it);
+    }
+  }
+}
+
+bool NTMechanismObjectModel::NTUpdate(const nt::EntryNotification& event,
+                                      std::string_view childName) {
+  if (event.entry == m_type) {
+    if ((event.flags & NT_NOTIFY_DELETE) != 0) {
+      return true;
+    }
+    if (event.value && event.value->IsString()) {
+      m_typeValue = event.value->GetString();
+    }
+  } else if (event.entry == m_color) {
+    if (event.value && event.value->IsString()) {
+      ConvertColor(event.value->GetString(), &m_colorValue);
+    }
+  } else if (event.entry == m_weight) {
+    if (event.value && event.value->IsDouble()) {
+      m_weightValue = event.value->GetDouble();
+    }
+  } else if (event.entry == m_angle) {
+    if (event.value && event.value->IsDouble()) {
+      m_angleValue = units::degree_t{event.value->GetDouble()};
+    }
+  } else if (event.entry == m_length) {
+    if (event.value && event.value->IsDouble()) {
+      m_lengthValue = units::meter_t{event.value->GetDouble()};
+    }
+  } else {
+    m_group.NTUpdate(event, childName);
+  }
+  return false;
+}
+
+class NTMechanism2DModel::RootModel final : public MechanismRootModel {
+ public:
+  RootModel(NT_Inst inst, std::string_view path, std::string_view name)
+      : m_group{inst, path, name},
+        m_x{nt::GetEntry(inst, fmt::format("{}/x", path))},
+        m_y{nt::GetEntry(inst, fmt::format("{}/y", path))} {}
+
+  const char* GetName() const final { return m_group.GetName(); }
+  void ForEachObject(
+      wpi::function_ref<void(MechanismObjectModel& model)> func) final {
+    m_group.ForEachObject(func);
+  }
+
+  bool NTUpdate(const nt::EntryNotification& event, std::string_view childName);
+
+  frc::Translation2d GetPosition() const override { return m_pos; };
+
+ private:
+  NTMechanismGroupImpl m_group;
+  NT_Entry m_x;
+  NT_Entry m_y;
+  frc::Translation2d m_pos;
+};
+
+bool NTMechanism2DModel::RootModel::NTUpdate(const nt::EntryNotification& event,
+                                             std::string_view childName) {
+  if ((event.flags & NT_NOTIFY_DELETE) != 0 &&
+      (event.entry == m_x || event.entry == m_y)) {
+    return true;
+  } else if (event.entry == m_x) {
+    if (event.value && event.value->IsDouble()) {
+      m_pos = frc::Translation2d{units::meter_t{event.value->GetDouble()},
+                                 m_pos.Y()};
+    }
+  } else if (event.entry == m_y) {
+    if (event.value && event.value->IsDouble()) {
+      m_pos = frc::Translation2d{m_pos.X(),
+                                 units::meter_t{event.value->GetDouble()}};
+    }
+  } else {
+    m_group.NTUpdate(event, childName);
+  }
+  return false;
+}
+
+NTMechanism2DModel::NTMechanism2DModel(std::string_view path)
+    : NTMechanism2DModel{nt::GetDefaultInstance(), path} {}
+
+NTMechanism2DModel::NTMechanism2DModel(NT_Inst inst, std::string_view path)
+    : m_nt{inst},
+      m_path{fmt::format("{}/", path)},
+      m_name{m_nt.GetEntry(fmt::format("{}/.name", path))},
+      m_dimensions{m_nt.GetEntry(fmt::format("{}/dims", path))},
+      m_bgColor{m_nt.GetEntry(fmt::format("{}/backgroundColor", path))},
+      m_dimensionsValue{1_m, 1_m} {
+  m_nt.AddListener(m_path, NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE |
+                               NT_NOTIFY_UPDATE | NT_NOTIFY_IMMEDIATE);
+}
+
+NTMechanism2DModel::~NTMechanism2DModel() = default;
+
+void NTMechanism2DModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    // .name
+    if (event.entry == m_name) {
+      if (event.value && event.value->IsString()) {
+        m_nameValue = event.value->GetString();
+      }
+      continue;
+    }
+
+    // dims
+    if (event.entry == m_dimensions) {
+      if (event.value && event.value->IsDoubleArray()) {
+        auto arr = event.value->GetDoubleArray();
+        if (arr.size() == 2) {
+          m_dimensionsValue = frc::Translation2d{units::meter_t{arr[0]},
+                                                 units::meter_t{arr[1]}};
+        }
+      }
+    }
+
+    // backgroundColor
+    if (event.entry == m_bgColor) {
+      if (event.value && event.value->IsString()) {
+        ConvertColor(event.value->GetString(), &m_bgColorValue);
+      }
+    }
+
+    std::string_view name = event.name;
+    if (wpi::starts_with(name, m_path)) {
+      name.remove_prefix(m_path.size());
+      if (name.empty() || name[0] == '.') {
+        continue;
+      }
+      std::string_view childName;
+      std::tie(name, childName) = wpi::split(name, '/');
+      if (childName.empty()) {
+        continue;
+      }
+
+      auto it = std::lower_bound(m_roots.begin(), m_roots.end(), name,
+                                 [](const auto& e, std::string_view name) {
+                                   return e->GetName() < name;
+                                 });
+      bool match = it != m_roots.end() && (*it)->GetName() == name;
+
+      if (event.flags & NT_NOTIFY_NEW) {
+        if (!match) {
+          it = m_roots.emplace(
+              it,
+              std::make_unique<RootModel>(
+                  m_nt.GetInstance(), fmt::format("{}{}", m_path, name), name));
+          match = true;
+        }
+      }
+      if (match) {
+        if ((*it)->NTUpdate(event, childName)) {
+          m_roots.erase(it);
+        }
+      }
+    }
+  }
+}
+
+bool NTMechanism2DModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_name) != NT_UNASSIGNED;
+}
+
+bool NTMechanism2DModel::IsReadOnly() {
+  return false;
+}
+
+void NTMechanism2DModel::ForEachRoot(
+    wpi::function_ref<void(MechanismRootModel& model)> func) {
+  for (auto&& obj : m_roots) {
+    func(*obj);
+  }
+}
diff --git a/glass/src/libnt/native/cpp/NTPIDController.cpp b/glass/src/libnt/native/cpp/NTPIDController.cpp
new file mode 100644
index 0000000..7936057
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTPIDController.cpp
@@ -0,0 +1,85 @@
+// 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/networktables/NTPIDController.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTPIDControllerModel::NTPIDControllerModel(std::string_view path)
+    : NTPIDControllerModel(nt::GetDefaultInstance(), path) {}
+
+NTPIDControllerModel::NTPIDControllerModel(NT_Inst instance,
+                                           std::string_view path)
+    : m_nt(instance),
+      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
+      m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))),
+      m_p(m_nt.GetEntry(fmt::format("{}/p", path))),
+      m_i(m_nt.GetEntry(fmt::format("{}/i", path))),
+      m_d(m_nt.GetEntry(fmt::format("{}/d", path))),
+      m_setpoint(m_nt.GetEntry(fmt::format("{}/setpoint", path))),
+      m_pData(fmt::format("NTPIDCtrlP:{}", path)),
+      m_iData(fmt::format("NTPIDCtrlI:{}", path)),
+      m_dData(fmt::format("NTPIDCtrlD:{}", path)),
+      m_setpointData(fmt::format("NTPIDCtrlStpt:{}", path)),
+      m_nameValue(wpi::rsplit(path, '/').second) {
+  m_nt.AddListener(m_name);
+  m_nt.AddListener(m_controllable);
+  m_nt.AddListener(m_p);
+  m_nt.AddListener(m_i);
+  m_nt.AddListener(m_d);
+  m_nt.AddListener(m_setpoint);
+}
+
+void NTPIDControllerModel::SetP(double value) {
+  nt::SetEntryValue(m_p, nt::NetworkTableValue::MakeDouble(value));
+}
+
+void NTPIDControllerModel::SetI(double value) {
+  nt::SetEntryValue(m_i, nt::NetworkTableValue::MakeDouble(value));
+}
+
+void NTPIDControllerModel::SetD(double value) {
+  nt::SetEntryValue(m_d, nt::NetworkTableValue::MakeDouble(value));
+}
+
+void NTPIDControllerModel::SetSetpoint(double value) {
+  nt::SetEntryValue(m_setpoint, nt::NetworkTableValue::MakeDouble(value));
+}
+
+void NTPIDControllerModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_name) {
+      if (event.value && event.value->IsString()) {
+        m_nameValue = event.value->GetString();
+      }
+    } else if (event.entry == m_p) {
+      if (event.value && event.value->IsDouble()) {
+        m_pData.SetValue(event.value->GetDouble());
+      }
+    } else if (event.entry == m_i) {
+      if (event.value && event.value->IsDouble()) {
+        m_iData.SetValue(event.value->GetDouble());
+      }
+    } else if (event.entry == m_d) {
+      if (event.value && event.value->IsDouble()) {
+        m_dData.SetValue(event.value->GetDouble());
+      }
+    } else if (event.entry == m_setpoint) {
+      if (event.value && event.value->IsDouble()) {
+        m_setpointData.SetValue(event.value->GetDouble());
+      }
+    } else if (event.entry == m_controllable) {
+      if (event.value && event.value->IsBoolean()) {
+        m_controllableValue = event.value->GetBoolean();
+      }
+    }
+  }
+}
+
+bool NTPIDControllerModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_setpoint) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTSpeedController.cpp b/glass/src/libnt/native/cpp/NTSpeedController.cpp
new file mode 100644
index 0000000..3dc351a
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTSpeedController.cpp
@@ -0,0 +1,52 @@
+// 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/networktables/NTSpeedController.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTSpeedControllerModel::NTSpeedControllerModel(std::string_view path)
+    : NTSpeedControllerModel(nt::GetDefaultInstance(), path) {}
+
+NTSpeedControllerModel::NTSpeedControllerModel(NT_Inst instance,
+                                               std::string_view path)
+    : m_nt(instance),
+      m_value(m_nt.GetEntry(fmt::format("{}/Value", path))),
+      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
+      m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))),
+      m_valueData(fmt::format("NT_SpdCtrl:{}", path)),
+      m_nameValue(wpi::rsplit(path, '/').second) {
+  m_nt.AddListener(m_value);
+  m_nt.AddListener(m_name);
+  m_nt.AddListener(m_controllable);
+}
+
+void NTSpeedControllerModel::SetPercent(double value) {
+  nt::SetEntryValue(m_value, nt::NetworkTableValue::MakeDouble(value));
+}
+
+void NTSpeedControllerModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_value) {
+      if (event.value && event.value->IsDouble()) {
+        m_valueData.SetValue(event.value->GetDouble());
+      }
+    } else if (event.entry == m_name) {
+      if (event.value && event.value->IsString()) {
+        m_nameValue = event.value->GetString();
+      }
+    } else if (event.entry == m_controllable) {
+      if (event.value && event.value->IsBoolean()) {
+        m_controllableValue = event.value->GetBoolean();
+      }
+    }
+  }
+}
+
+bool NTSpeedControllerModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTStringChooser.cpp b/glass/src/libnt/native/cpp/NTStringChooser.cpp
new file mode 100644
index 0000000..e6a97fa
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTStringChooser.cpp
@@ -0,0 +1,75 @@
+// 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/networktables/NTStringChooser.h"
+
+#include <fmt/format.h>
+
+using namespace glass;
+
+NTStringChooserModel::NTStringChooserModel(std::string_view path)
+    : NTStringChooserModel{nt::GetDefaultInstance(), path} {}
+
+NTStringChooserModel::NTStringChooserModel(NT_Inst inst, std::string_view path)
+    : m_nt{inst},
+      m_default{m_nt.GetEntry(fmt::format("{}/default", path))},
+      m_selected{m_nt.GetEntry(fmt::format("{}/selected", path))},
+      m_active{m_nt.GetEntry(fmt::format("{}/active", path))},
+      m_options{m_nt.GetEntry(fmt::format("{}/options", path))} {
+  m_nt.AddListener(m_default);
+  m_nt.AddListener(m_selected);
+  m_nt.AddListener(m_active);
+  m_nt.AddListener(m_options);
+}
+
+void NTStringChooserModel::SetDefault(std::string_view val) {
+  nt::SetEntryValue(m_default, nt::Value::MakeString(val));
+}
+
+void NTStringChooserModel::SetSelected(std::string_view val) {
+  nt::SetEntryValue(m_selected, nt::Value::MakeString(val));
+}
+
+void NTStringChooserModel::SetActive(std::string_view val) {
+  nt::SetEntryValue(m_active, nt::Value::MakeString(val));
+}
+
+void NTStringChooserModel::SetOptions(wpi::span<const std::string> val) {
+  nt::SetEntryValue(m_options, nt::Value::MakeStringArray(val));
+}
+
+void NTStringChooserModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_default) {
+      if ((event.flags & NT_NOTIFY_DELETE) != 0) {
+        m_defaultValue.clear();
+      } else if (event.value && event.value->IsString()) {
+        m_defaultValue = event.value->GetString();
+      }
+    } else if (event.entry == m_selected) {
+      if ((event.flags & NT_NOTIFY_DELETE) != 0) {
+        m_selectedValue.clear();
+      } else if (event.value && event.value->IsString()) {
+        m_selectedValue = event.value->GetString();
+      }
+    } else if (event.entry == m_active) {
+      if ((event.flags & NT_NOTIFY_DELETE) != 0) {
+        m_activeValue.clear();
+      } else if (event.value && event.value->IsString()) {
+        m_activeValue = event.value->GetString();
+      }
+    } else if (event.entry == m_options) {
+      if ((event.flags & NT_NOTIFY_DELETE) != 0) {
+        m_optionsValue.clear();
+      } else if (event.value && event.value->IsStringArray()) {
+        auto arr = event.value->GetStringArray();
+        m_optionsValue.assign(arr.begin(), arr.end());
+      }
+    }
+  }
+}
+
+bool NTStringChooserModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_options) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTSubsystem.cpp b/glass/src/libnt/native/cpp/NTSubsystem.cpp
new file mode 100644
index 0000000..b2bdf8c
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTSubsystem.cpp
@@ -0,0 +1,45 @@
+// 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/networktables/NTSubsystem.h"
+
+#include <fmt/format.h>
+
+using namespace glass;
+
+NTSubsystemModel::NTSubsystemModel(std::string_view path)
+    : NTSubsystemModel(nt::GetDefaultInstance(), path) {}
+
+NTSubsystemModel::NTSubsystemModel(NT_Inst instance, std::string_view path)
+    : m_nt(instance),
+      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
+      m_defaultCommand(m_nt.GetEntry(fmt::format("{}/.default", path))),
+      m_currentCommand(m_nt.GetEntry(fmt::format("{}/.command", path))) {
+  m_nt.AddListener(m_name);
+  m_nt.AddListener(m_defaultCommand);
+  m_nt.AddListener(m_currentCommand);
+}
+
+void NTSubsystemModel::Update() {
+  for (auto&& event : m_nt.PollListener()) {
+    if (event.entry == m_name) {
+      if (event.value && event.value->IsString()) {
+        m_nameValue = event.value->GetString();
+      }
+    } else if (event.entry == m_defaultCommand) {
+      if (event.value && event.value->IsString()) {
+        m_defaultCommandValue = event.value->GetString();
+      }
+    } else if (event.entry == m_currentCommand) {
+      if (event.value && event.value->IsString()) {
+        m_currentCommandValue = event.value->GetString();
+      }
+    }
+  }
+}
+
+bool NTSubsystemModel::Exists() {
+  return m_nt.IsConnected() &&
+         nt::GetEntryType(m_defaultCommand) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NetworkTables.cpp b/glass/src/libnt/native/cpp/NetworkTables.cpp
new file mode 100644
index 0000000..596ef0a
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NetworkTables.cpp
@@ -0,0 +1,780 @@
+// 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/networktables/NetworkTables.h"
+
+#include <networktables/NetworkTableValue.h>
+
+#include <cinttypes>
+#include <cstdio>
+#include <cstring>
+#include <initializer_list>
+#include <memory>
+#include <string_view>
+#include <vector>
+
+#include <fmt/format.h>
+#include <imgui.h>
+#include <ntcore_cpp.h>
+#include <wpi/SmallString.h>
+#include <wpi/SpanExtras.h>
+#include <wpi/StringExtras.h>
+#include <wpi/raw_ostream.h>
+#include <wpi/span.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+static std::string BooleanArrayToString(wpi::span<const int> in) {
+  std::string rv;
+  wpi::raw_string_ostream os{rv};
+  os << '[';
+  bool first = true;
+  for (auto v : in) {
+    if (!first) {
+      os << ',';
+    }
+    first = false;
+    if (v) {
+      os << "true";
+    } else {
+      os << "false";
+    }
+  }
+  os << ']';
+  return rv;
+}
+
+static std::string DoubleArrayToString(wpi::span<const double> in) {
+  return fmt::format("[{:.6f}]", fmt::join(in, ","));
+}
+
+static std::string StringArrayToString(wpi::span<const std::string> in) {
+  std::string rv;
+  wpi::raw_string_ostream os{rv};
+  os << '[';
+  bool first = true;
+  for (auto&& v : in) {
+    if (!first) {
+      os << ',';
+    }
+    first = false;
+    os << '"';
+    os.write_escaped(v);
+    os << '"';
+  }
+  os << ']';
+  return rv;
+}
+
+NetworkTablesModel::NetworkTablesModel()
+    : NetworkTablesModel{nt::GetDefaultInstance()} {}
+
+NetworkTablesModel::NetworkTablesModel(NT_Inst inst)
+    : m_inst{inst}, m_poller{nt::CreateEntryListenerPoller(inst)} {
+  nt::AddPolledEntryListener(m_poller, "",
+                             NT_NOTIFY_LOCAL | NT_NOTIFY_NEW |
+                                 NT_NOTIFY_UPDATE | NT_NOTIFY_DELETE |
+                                 NT_NOTIFY_FLAGS | NT_NOTIFY_IMMEDIATE);
+}
+
+NetworkTablesModel::~NetworkTablesModel() {
+  nt::DestroyEntryListenerPoller(m_poller);
+}
+
+NetworkTablesModel::Entry::Entry(nt::EntryNotification&& event)
+    : entry{event.entry},
+      name{std::move(event.name)},
+      value{std::move(event.value)},
+      flags{nt::GetEntryFlags(event.entry)} {
+  UpdateValue();
+}
+
+void NetworkTablesModel::Entry::UpdateValue() {
+  switch (value->type()) {
+    case NT_BOOLEAN:
+      if (!source) {
+        source = std::make_unique<DataSource>(fmt::format("NT:{}", name));
+      }
+      source->SetValue(value->GetBoolean() ? 1 : 0);
+      source->SetDigital(true);
+      break;
+    case NT_DOUBLE:
+      if (!source) {
+        source = std::make_unique<DataSource>(fmt::format("NT:{}", name));
+      }
+      source->SetValue(value->GetDouble());
+      source->SetDigital(false);
+      break;
+    case NT_BOOLEAN_ARRAY:
+      valueStr = BooleanArrayToString(value->GetBooleanArray());
+      break;
+    case NT_DOUBLE_ARRAY:
+      valueStr = DoubleArrayToString(value->GetDoubleArray());
+      break;
+    case NT_STRING_ARRAY:
+      valueStr = StringArrayToString(value->GetStringArray());
+      break;
+    default:
+      break;
+  }
+}
+
+void NetworkTablesModel::Update() {
+  bool timedOut = false;
+  bool updateTree = false;
+  for (auto&& event : nt::PollEntryListener(m_poller, 0, &timedOut)) {
+    auto& entry = m_entries[event.entry];
+    if (event.flags & NT_NOTIFY_NEW) {
+      if (!entry) {
+        entry = std::make_unique<Entry>(std::move(event));
+        m_sortedEntries.emplace_back(entry.get());
+        updateTree = true;
+        continue;
+      }
+    }
+    if (!entry) {
+      continue;
+    }
+    if (event.flags & NT_NOTIFY_DELETE) {
+      auto it = std::find(m_sortedEntries.begin(), m_sortedEntries.end(),
+                          entry.get());
+      // will be removed completely below
+      if (it != m_sortedEntries.end()) {
+        *it = nullptr;
+      }
+      m_entries.erase(event.entry);
+      updateTree = true;
+      continue;
+    }
+    if (event.flags & NT_NOTIFY_UPDATE) {
+      entry->value = std::move(event.value);
+      entry->UpdateValue();
+    }
+    if (event.flags & NT_NOTIFY_FLAGS) {
+      entry->flags = nt::GetEntryFlags(event.entry);
+    }
+  }
+
+  // shortcut common case (updates)
+  if (!updateTree) {
+    return;
+  }
+
+  // remove deleted entries
+  m_sortedEntries.erase(
+      std::remove(m_sortedEntries.begin(), m_sortedEntries.end(), nullptr),
+      m_sortedEntries.end());
+
+  // sort by name
+  std::sort(m_sortedEntries.begin(), m_sortedEntries.end(),
+            [](const auto& a, const auto& b) { return a->name < b->name; });
+
+  // rebuild tree
+  m_root.clear();
+  wpi::SmallVector<std::string_view, 16> parts;
+  for (auto& entry : m_sortedEntries) {
+    parts.clear();
+    wpi::split(entry->name, parts, '/', -1, false);
+
+    // ignore a raw "/" key
+    if (parts.empty()) {
+      continue;
+    }
+
+    // get to leaf
+    auto nodes = &m_root;
+    for (auto part : wpi::drop_back(wpi::span{parts.begin(), parts.end()})) {
+      auto it =
+          std::find_if(nodes->begin(), nodes->end(),
+                       [&](const auto& node) { return node.name == part; });
+      if (it == nodes->end()) {
+        nodes->emplace_back(part);
+        // path is from the beginning of the string to the end of the current
+        // part; this works because part is a reference to the internals of
+        // entry->name
+        nodes->back().path.assign(
+            entry->name.data(), part.data() + part.size() - entry->name.data());
+        it = nodes->end() - 1;
+      }
+      nodes = &it->children;
+    }
+
+    auto it = std::find_if(nodes->begin(), nodes->end(), [&](const auto& node) {
+      return node.name == parts.back();
+    });
+    if (it == nodes->end()) {
+      nodes->emplace_back(parts.back());
+      // no need to set path, as it's identical to entry->name
+      it = nodes->end() - 1;
+    }
+    it->entry = entry;
+  }
+}
+
+bool NetworkTablesModel::Exists() {
+  return nt::IsConnected(m_inst);
+}
+
+static std::shared_ptr<nt::Value> StringToBooleanArray(std::string_view in) {
+  in = wpi::trim(in);
+  if (in.empty()) {
+    return nt::NetworkTableValue::MakeBooleanArray(
+        std::initializer_list<bool>{});
+  }
+  if (in.front() == '[') {
+    in.remove_prefix(1);
+  }
+  if (in.back() == ']') {
+    in.remove_suffix(1);
+  }
+  in = wpi::trim(in);
+
+  wpi::SmallVector<std::string_view, 16> inSplit;
+  wpi::SmallVector<int, 16> out;
+
+  wpi::split(in, inSplit, ',', -1, false);
+  for (auto val : inSplit) {
+    val = wpi::trim(val);
+    if (wpi::equals_lower(val, "true")) {
+      out.emplace_back(1);
+    } else if (wpi::equals_lower(val, "false")) {
+      out.emplace_back(0);
+    } else {
+      fmt::print(stderr,
+                 "GUI: NetworkTables: Could not understand value '{}'\n", val);
+      return nullptr;
+    }
+  }
+
+  return nt::NetworkTableValue::MakeBooleanArray(out);
+}
+
+static std::shared_ptr<nt::Value> StringToDoubleArray(std::string_view in) {
+  in = wpi::trim(in);
+  if (in.empty()) {
+    return nt::NetworkTableValue::MakeDoubleArray(
+        std::initializer_list<double>{});
+  }
+  if (in.front() == '[') {
+    in.remove_prefix(1);
+  }
+  if (in.back() == ']') {
+    in.remove_suffix(1);
+  }
+  in = wpi::trim(in);
+
+  wpi::SmallVector<std::string_view, 16> inSplit;
+  wpi::SmallVector<double, 16> out;
+
+  wpi::split(in, inSplit, ',', -1, false);
+  for (auto val : inSplit) {
+    if (auto num = wpi::parse_float<double>(wpi::trim(val))) {
+      out.emplace_back(num.value());
+    } else {
+      fmt::print(stderr,
+                 "GUI: NetworkTables: Could not understand value '{}'\n", val);
+      return nullptr;
+    }
+  }
+
+  return nt::NetworkTableValue::MakeDoubleArray(out);
+}
+
+static int fromxdigit(char ch) {
+  if (ch >= 'a' && ch <= 'f') {
+    return (ch - 'a' + 10);
+  } else if (ch >= 'A' && ch <= 'F') {
+    return (ch - 'A' + 10);
+  } else {
+    return ch - '0';
+  }
+}
+
+static std::string_view UnescapeString(std::string_view source,
+                                       wpi::SmallVectorImpl<char>& buf) {
+  assert(source.size() >= 2 && source.front() == '"' && source.back() == '"');
+  buf.clear();
+  buf.reserve(source.size() - 2);
+  for (auto s = source.begin() + 1, end = source.end() - 1; s != end; ++s) {
+    if (*s != '\\') {
+      buf.push_back(*s);
+      continue;
+    }
+    switch (*++s) {
+      case 't':
+        buf.push_back('\t');
+        break;
+      case 'n':
+        buf.push_back('\n');
+        break;
+      case 'x': {
+        if (!isxdigit(*(s + 1))) {
+          buf.push_back('x');  // treat it like a unknown escape
+          break;
+        }
+        int ch = fromxdigit(*++s);
+        if (std::isxdigit(*(s + 1))) {
+          ch <<= 4;
+          ch |= fromxdigit(*++s);
+        }
+        buf.push_back(static_cast<char>(ch));
+        break;
+      }
+      default:
+        buf.push_back(*s);
+        break;
+    }
+  }
+  return {buf.data(), buf.size()};
+}
+
+static std::shared_ptr<nt::Value> StringToStringArray(std::string_view in) {
+  in = wpi::trim(in);
+  if (in.empty()) {
+    return nt::NetworkTableValue::MakeStringArray(
+        std::initializer_list<std::string>{});
+  }
+  if (in.front() == '[') {
+    in.remove_prefix(1);
+  }
+  if (in.back() == ']') {
+    in.remove_suffix(1);
+  }
+  in = wpi::trim(in);
+
+  wpi::SmallVector<std::string_view, 16> inSplit;
+  std::vector<std::string> out;
+  wpi::SmallString<32> buf;
+
+  wpi::split(in, inSplit, ',', -1, false);
+  for (auto val : inSplit) {
+    val = wpi::trim(val);
+    if (val.empty()) {
+      continue;
+    }
+    if (val.front() != '"' || val.back() != '"') {
+      fmt::print(stderr,
+                 "GUI: NetworkTables: Could not understand value '{}'\n", val);
+      return nullptr;
+    }
+    out.emplace_back(UnescapeString(val, buf));
+  }
+
+  return nt::NetworkTableValue::MakeStringArray(std::move(out));
+}
+
+static void EmitEntryValueReadonly(NetworkTablesModel::Entry& entry) {
+  auto& val = entry.value;
+  if (!val) {
+    return;
+  }
+
+  switch (val->type()) {
+    case NT_BOOLEAN:
+      ImGui::LabelText("boolean", "%s", val->GetBoolean() ? "true" : "false");
+      break;
+    case NT_DOUBLE:
+      ImGui::LabelText("double", "%.6f", val->GetDouble());
+      break;
+    case NT_STRING: {
+      // GetString() comes from a std::string, so it's null terminated
+      ImGui::LabelText("string", "%s", val->GetString().data());
+      break;
+    }
+    case NT_BOOLEAN_ARRAY:
+      ImGui::LabelText("boolean[]", "%s", entry.valueStr.c_str());
+      break;
+    case NT_DOUBLE_ARRAY:
+      ImGui::LabelText("double[]", "%s", entry.valueStr.c_str());
+      break;
+    case NT_STRING_ARRAY:
+      ImGui::LabelText("string[]", "%s", entry.valueStr.c_str());
+      break;
+    case NT_RAW:
+      ImGui::LabelText("raw", "[...]");
+      break;
+    case NT_RPC:
+      ImGui::LabelText("rpc", "[...]");
+      break;
+    default:
+      ImGui::LabelText("other", "?");
+      break;
+  }
+}
+
+static constexpr size_t kTextBufferSize = 4096;
+
+static char* GetTextBuffer(std::string_view in) {
+  static char textBuffer[kTextBufferSize];
+  size_t len = (std::min)(in.size(), kTextBufferSize - 1);
+  std::memcpy(textBuffer, in.data(), len);
+  textBuffer[len] = '\0';
+  return textBuffer;
+}
+
+static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry) {
+  auto& val = entry.value;
+  if (!val) {
+    return;
+  }
+
+  ImGui::PushID(entry.name.c_str());
+  switch (val->type()) {
+    case NT_BOOLEAN: {
+      static const char* boolOptions[] = {"false", "true"};
+      int v = val->GetBoolean() ? 1 : 0;
+      if (ImGui::Combo("boolean", &v, boolOptions, 2)) {
+        nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeBoolean(v));
+      }
+      break;
+    }
+    case NT_DOUBLE: {
+      double v = val->GetDouble();
+      if (ImGui::InputDouble("double", &v, 0, 0, "%.6f",
+                             ImGuiInputTextFlags_EnterReturnsTrue)) {
+        nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeDouble(v));
+      }
+      break;
+    }
+    case NT_STRING: {
+      char* v = GetTextBuffer(val->GetString());
+      if (ImGui::InputText("string", v, kTextBufferSize,
+                           ImGuiInputTextFlags_EnterReturnsTrue)) {
+        nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeString(v));
+      }
+      break;
+    }
+    case NT_BOOLEAN_ARRAY: {
+      char* v = GetTextBuffer(entry.valueStr);
+      if (ImGui::InputText("boolean[]", v, kTextBufferSize,
+                           ImGuiInputTextFlags_EnterReturnsTrue)) {
+        if (auto outv = StringToBooleanArray(v)) {
+          nt::SetEntryValue(entry.entry, std::move(outv));
+        }
+      }
+      break;
+    }
+    case NT_DOUBLE_ARRAY: {
+      char* v = GetTextBuffer(entry.valueStr);
+      if (ImGui::InputText("double[]", v, kTextBufferSize,
+                           ImGuiInputTextFlags_EnterReturnsTrue)) {
+        if (auto outv = StringToDoubleArray(v)) {
+          nt::SetEntryValue(entry.entry, std::move(outv));
+        }
+      }
+      break;
+    }
+    case NT_STRING_ARRAY: {
+      char* v = GetTextBuffer(entry.valueStr);
+      if (ImGui::InputText("string[]", v, kTextBufferSize,
+                           ImGuiInputTextFlags_EnterReturnsTrue)) {
+        if (auto outv = StringToStringArray(v)) {
+          nt::SetEntryValue(entry.entry, std::move(outv));
+        }
+      }
+      break;
+    }
+    case NT_RAW:
+      ImGui::LabelText("raw", "[...]");
+      break;
+    case NT_RPC:
+      ImGui::LabelText("rpc", "[...]");
+      break;
+    default:
+      ImGui::LabelText("other", "?");
+      break;
+  }
+  ImGui::PopID();
+}
+
+static void EmitParentContextMenu(const std::string& path,
+                                  NetworkTablesFlags flags) {
+  // Workaround https://github.com/ocornut/imgui/issues/331
+  bool openWarningPopup = false;
+  static char nameBuffer[kTextBufferSize];
+  if (ImGui::BeginPopupContextItem(path.c_str())) {
+    ImGui::Text("%s", path.c_str());
+    ImGui::Separator();
+
+    if (ImGui::BeginMenu("Add new...")) {
+      if (ImGui::IsWindowAppearing()) {
+        nameBuffer[0] = '\0';
+      }
+
+      ImGui::InputTextWithHint("New item name", "example", nameBuffer,
+                               kTextBufferSize);
+      std::string fullNewPath;
+      if (path == "/") {
+        fullNewPath = path + nameBuffer;
+      } else {
+        fullNewPath = fmt::format("{}/{}", path, nameBuffer);
+      }
+
+      ImGui::Text("Adding: %s", fullNewPath.c_str());
+      ImGui::Separator();
+      auto entry = nt::GetEntry(nt::GetDefaultInstance(), fullNewPath);
+      bool enabled = (flags & NetworkTablesFlags_CreateNoncanonicalKeys ||
+                      nameBuffer[0] != '\0') &&
+                     nt::GetEntryType(entry) == NT_Type::NT_UNASSIGNED;
+      if (ImGui::MenuItem("string", nullptr, false, enabled)) {
+        if (!nt::SetEntryValue(entry, nt::Value::MakeString(""))) {
+          openWarningPopup = true;
+        }
+      }
+      if (ImGui::MenuItem("double", nullptr, false, enabled)) {
+        if (!nt::SetEntryValue(entry, nt::Value::MakeDouble(0.0))) {
+          openWarningPopup = true;
+        }
+      }
+      if (ImGui::MenuItem("boolean", nullptr, false, enabled)) {
+        if (!nt::SetEntryValue(entry, nt::Value::MakeBoolean(false))) {
+          openWarningPopup = true;
+        }
+      }
+      if (ImGui::MenuItem("string[]", nullptr, false, enabled)) {
+        if (!nt::SetEntryValue(entry, nt::Value::MakeStringArray({""}))) {
+          openWarningPopup = true;
+        }
+      }
+      if (ImGui::MenuItem("double[]", nullptr, false, enabled)) {
+        if (!nt::SetEntryValue(entry, nt::Value::MakeDoubleArray({0.0}))) {
+          openWarningPopup = true;
+        }
+      }
+      if (ImGui::MenuItem("boolean[]", nullptr, false, enabled)) {
+        if (!nt::SetEntryValue(entry, nt::Value::MakeBooleanArray({false}))) {
+          openWarningPopup = true;
+        }
+      }
+
+      ImGui::EndMenu();
+    }
+
+    ImGui::Separator();
+    if (ImGui::MenuItem("Remove All")) {
+      for (auto&& entry : nt::GetEntries(nt::GetDefaultInstance(), path, 0)) {
+        nt::DeleteEntry(entry);
+      }
+    }
+    ImGui::EndPopup();
+  }
+
+  // Workaround https://github.com/ocornut/imgui/issues/331
+  if (openWarningPopup) {
+    ImGui::OpenPopup("Value exists");
+  }
+  if (ImGui::BeginPopupModal("Value exists", nullptr,
+                             ImGuiWindowFlags_AlwaysAutoResize)) {
+    ImGui::Text("The provided name %s already exists in the tree!", nameBuffer);
+    ImGui::Separator();
+
+    if (ImGui::Button("OK", ImVec2(120, 0))) {
+      ImGui::CloseCurrentPopup();
+    }
+    ImGui::SetItemDefaultFocus();
+    ImGui::EndPopup();
+  }
+}
+
+static void EmitEntry(NetworkTablesModel::Entry& entry, const char* name,
+                      NetworkTablesFlags flags) {
+  if (entry.source) {
+    ImGui::Selectable(name);
+    entry.source->EmitDrag();
+  } else {
+    ImGui::Text("%s", name);
+  }
+  if (ImGui::BeginPopupContextItem(entry.name.c_str())) {
+    ImGui::Text("%s", entry.name.c_str());
+    ImGui::Separator();
+    if (ImGui::MenuItem("Remove")) {
+      nt::DeleteEntry(entry.entry);
+    }
+    ImGui::EndPopup();
+  }
+  ImGui::NextColumn();
+
+  if (flags & NetworkTablesFlags_ReadOnly) {
+    EmitEntryValueReadonly(entry);
+  } else {
+    EmitEntryValueEditable(entry);
+  }
+  ImGui::NextColumn();
+
+  if (flags & NetworkTablesFlags_ShowFlags) {
+    if ((entry.flags & NT_PERSISTENT) != 0) {
+      ImGui::Text("Persistent");
+    } else if (entry.flags != 0) {
+      ImGui::Text("%02x", entry.flags);
+    }
+    ImGui::NextColumn();
+  }
+
+  if (flags & NetworkTablesFlags_ShowTimestamp) {
+    if (entry.value) {
+      ImGui::Text("%f", (entry.value->last_change() * 1.0e-6) -
+                            (GetZeroTime() * 1.0e-6));
+    } else {
+      ImGui::TextUnformatted("");
+    }
+    ImGui::NextColumn();
+  }
+  ImGui::Separator();
+}
+
+static void EmitTree(const std::vector<NetworkTablesModel::TreeNode>& tree,
+                     NetworkTablesFlags flags) {
+  for (auto&& node : tree) {
+    if (node.entry) {
+      EmitEntry(*node.entry, node.name.c_str(), flags);
+    }
+
+    if (!node.children.empty()) {
+      bool open =
+          TreeNodeEx(node.name.c_str(), ImGuiTreeNodeFlags_SpanFullWidth);
+      EmitParentContextMenu(node.path, flags);
+      ImGui::NextColumn();
+      ImGui::NextColumn();
+      if (flags & NetworkTablesFlags_ShowFlags) {
+        ImGui::NextColumn();
+      }
+      if (flags & NetworkTablesFlags_ShowTimestamp) {
+        ImGui::NextColumn();
+      }
+      ImGui::Separator();
+      if (open) {
+        EmitTree(node.children, flags);
+        TreePop();
+      }
+    }
+  }
+}
+
+void glass::DisplayNetworkTables(NetworkTablesModel* model,
+                                 NetworkTablesFlags flags) {
+  auto inst = model->GetInstance();
+
+  if (flags & NetworkTablesFlags_ShowConnections) {
+    if (CollapsingHeader("Connections")) {
+      ImGui::Columns(4, "connections");
+      ImGui::Text("Id");
+      ImGui::NextColumn();
+      ImGui::Text("Address");
+      ImGui::NextColumn();
+      ImGui::Text("Updated");
+      ImGui::NextColumn();
+      ImGui::Text("Proto");
+      ImGui::NextColumn();
+      ImGui::Separator();
+      for (auto&& i : nt::GetConnections(inst)) {
+        ImGui::Text("%s", i.remote_id.c_str());
+        ImGui::NextColumn();
+        ImGui::Text("%s", i.remote_ip.c_str());
+        ImGui::NextColumn();
+        ImGui::Text("%llu",
+                    static_cast<unsigned long long>(  // NOLINT(runtime/int)
+                        i.last_update));
+        ImGui::NextColumn();
+        ImGui::Text("%d.%d", i.protocol_version >> 8,
+                    i.protocol_version & 0xff);
+        ImGui::NextColumn();
+      }
+      ImGui::Columns();
+    }
+
+    if (!CollapsingHeader("Values", ImGuiTreeNodeFlags_DefaultOpen)) {
+      return;
+    }
+  }
+
+  const bool showFlags = (flags & NetworkTablesFlags_ShowFlags);
+  const bool showTimestamp = (flags & NetworkTablesFlags_ShowTimestamp);
+
+  static bool first = true;
+  ImGui::Columns(2 + (showFlags ? 1 : 0) + (showTimestamp ? 1 : 0), "values");
+  if (first) {
+    ImGui::SetColumnWidth(-1, 0.5f * ImGui::GetWindowWidth());
+  }
+  ImGui::Text("Name");
+  EmitParentContextMenu("/", flags);
+  ImGui::NextColumn();
+  ImGui::Text("Value");
+  ImGui::NextColumn();
+  if (showFlags) {
+    if (first) {
+      ImGui::SetColumnWidth(-1, 12 * ImGui::GetFontSize());
+    }
+    ImGui::Text("Flags");
+    ImGui::NextColumn();
+  }
+  if (showTimestamp) {
+    ImGui::Text("Changed");
+    ImGui::NextColumn();
+  }
+  ImGui::Separator();
+  first = false;
+
+  if (flags & NetworkTablesFlags_TreeView) {
+    EmitTree(model->GetTreeRoot(), flags);
+  } else {
+    for (auto entry : model->GetEntries()) {
+      EmitEntry(*entry, entry->name.c_str(), flags);
+    }
+  }
+  ImGui::Columns();
+}
+
+void NetworkTablesFlagsSettings::Update() {
+  if (!m_pTreeView) {
+    auto& storage = GetStorage();
+    m_pTreeView = storage.GetBoolRef(
+        "tree", m_defaultFlags & NetworkTablesFlags_TreeView);
+    m_pShowConnections = storage.GetBoolRef(
+        "connections", m_defaultFlags & NetworkTablesFlags_ShowConnections);
+    m_pShowFlags = storage.GetBoolRef(
+        "flags", m_defaultFlags & NetworkTablesFlags_ShowFlags);
+    m_pShowTimestamp = storage.GetBoolRef(
+        "timestamp", m_defaultFlags & NetworkTablesFlags_ShowTimestamp);
+    m_pCreateNoncanonicalKeys = storage.GetBoolRef(
+        "createNonCanonical",
+        m_defaultFlags & NetworkTablesFlags_CreateNoncanonicalKeys);
+  }
+
+  m_flags &=
+      ~(NetworkTablesFlags_TreeView | NetworkTablesFlags_ShowConnections |
+        NetworkTablesFlags_ShowFlags | NetworkTablesFlags_ShowTimestamp |
+        NetworkTablesFlags_CreateNoncanonicalKeys);
+  m_flags |=
+      (*m_pTreeView ? NetworkTablesFlags_TreeView : 0) |
+      (*m_pShowConnections ? NetworkTablesFlags_ShowConnections : 0) |
+      (*m_pShowFlags ? NetworkTablesFlags_ShowFlags : 0) |
+      (*m_pShowTimestamp ? NetworkTablesFlags_ShowTimestamp : 0) |
+      (*m_pCreateNoncanonicalKeys ? NetworkTablesFlags_CreateNoncanonicalKeys
+                                  : 0);
+}
+
+void NetworkTablesFlagsSettings::DisplayMenu() {
+  if (!m_pTreeView) {
+    return;
+  }
+  ImGui::MenuItem("Tree View", "", m_pTreeView);
+  ImGui::MenuItem("Show Connections", "", m_pShowConnections);
+  ImGui::MenuItem("Show Flags", "", m_pShowFlags);
+  ImGui::MenuItem("Show Timestamp", "", m_pShowTimestamp);
+  ImGui::Separator();
+  ImGui::MenuItem("Allow creation of non-canonical keys", "",
+                  m_pCreateNoncanonicalKeys);
+}
+
+void NetworkTablesView::Display() {
+  m_flags.Update();
+  if (ImGui::BeginPopupContextItem()) {
+    m_flags.DisplayMenu();
+    ImGui::EndPopup();
+  }
+  DisplayNetworkTables(m_model, m_flags.GetFlags());
+}
diff --git a/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp b/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp
new file mode 100644
index 0000000..5cb5bbc
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp
@@ -0,0 +1,19 @@
+// 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/networktables/NetworkTablesHelper.h"
+
+using namespace glass;
+
+NetworkTablesHelper::NetworkTablesHelper(NT_Inst inst)
+    : m_inst{inst}, m_poller{nt::CreateEntryListenerPoller(inst)} {}
+
+NetworkTablesHelper::~NetworkTablesHelper() {
+  nt::DestroyEntryListenerPoller(m_poller);
+}
+
+bool NetworkTablesHelper::IsConnected() const {
+  return nt::GetNetworkMode(m_inst) == NT_NET_MODE_SERVER ||
+         nt::IsConnected(m_inst);
+}
diff --git a/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp b/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp
new file mode 100644
index 0000000..8d991cb
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp
@@ -0,0 +1,205 @@
+// 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/networktables/NetworkTablesProvider.h"
+
+#include <algorithm>
+
+#include <fmt/format.h>
+#include <ntcore_cpp.h>
+#include <wpi/SmallString.h>
+#include <wpi/StringExtras.h>
+#include <wpigui.h>
+
+using namespace glass;
+
+NetworkTablesProvider::NetworkTablesProvider(std::string_view iniName)
+    : NetworkTablesProvider{iniName, nt::GetDefaultInstance()} {}
+
+NetworkTablesProvider::NetworkTablesProvider(std::string_view iniName,
+                                             NT_Inst inst)
+    : Provider{fmt::format("{}Window", iniName)},
+      m_nt{inst},
+      m_typeCache{iniName} {
+  m_nt.AddListener("", NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE |
+                           NT_NOTIFY_IMMEDIATE);
+}
+
+void NetworkTablesProvider::GlobalInit() {
+  Provider::GlobalInit();
+  wpi::gui::AddInit([this] { m_typeCache.Initialize(); });
+}
+
+void NetworkTablesProvider::DisplayMenu() {
+  wpi::SmallVector<std::string_view, 6> path;
+  wpi::SmallString<64> name;
+  for (auto&& entry : m_viewEntries) {
+    path.clear();
+    wpi::split(entry->name, path, '/', -1, false);
+
+    bool fullDepth = true;
+    int depth = 0;
+    for (; depth < (static_cast<int>(path.size()) - 1); ++depth) {
+      name = path[depth];
+      if (!ImGui::BeginMenu(name.c_str())) {
+        fullDepth = false;
+        break;
+      }
+    }
+
+    if (fullDepth) {
+      bool visible = entry->window && entry->window->IsVisible();
+      bool wasVisible = visible;
+      // FIXME: enabled?
+      // data is the last item, so is guaranteed to be null-terminated
+      ImGui::MenuItem(path.back().data(), nullptr, &visible, true);
+      if (!wasVisible && visible) {
+        Show(entry.get(), entry->window);
+      } else if (wasVisible && !visible && entry->window) {
+        entry->window->SetVisible(false);
+      }
+    }
+
+    for (; depth > 0; --depth) {
+      ImGui::EndMenu();
+    }
+  }
+}
+
+void NetworkTablesProvider::Update() {
+  Provider::Update();
+
+  // add/remove entries from NT changes
+  for (auto&& event : m_nt.PollListener()) {
+    // look for .type fields
+    std::string_view eventName{event.name};
+    if (!wpi::ends_with(eventName, "/.type") || !event.value ||
+        !event.value->IsString()) {
+      continue;
+    }
+    auto tableName = wpi::drop_back(eventName, 6);
+
+    // only handle ones where we have a builder
+    auto builderIt = m_typeMap.find(event.value->GetString());
+    if (builderIt == m_typeMap.end()) {
+      continue;
+    }
+
+    if (event.flags & NT_NOTIFY_DELETE) {
+      auto it = std::find_if(
+          m_viewEntries.begin(), m_viewEntries.end(), [&](const auto& elem) {
+            return static_cast<Entry*>(elem->modelEntry)->typeEntry ==
+                   event.entry;
+          });
+      if (it != m_viewEntries.end()) {
+        m_viewEntries.erase(it);
+      }
+    } else if (event.flags & NT_NOTIFY_NEW) {
+      GetOrCreateView(builderIt->second, event.entry, tableName);
+      // cache the type
+      m_typeCache[tableName].SetName(event.value->GetString());
+    }
+  }
+
+  // check for visible windows that need displays (typically this is due to
+  // file loading)
+  for (auto&& window : m_windows) {
+    if (!window->IsVisible() || window->HasView()) {
+      continue;
+    }
+    auto id = window->GetId();
+    auto typeIt = m_typeCache.find(id);
+    if (typeIt == m_typeCache.end()) {
+      continue;
+    }
+
+    // only handle ones where we have a builder
+    auto builderIt = m_typeMap.find(typeIt->second.GetName());
+    if (builderIt == m_typeMap.end()) {
+      continue;
+    }
+
+    auto entry = GetOrCreateView(
+        builderIt->second,
+        nt::GetEntry(m_nt.GetInstance(), fmt::format("{}/.type", id)), id);
+    if (entry) {
+      Show(entry, window.get());
+    }
+  }
+}
+
+void NetworkTablesProvider::Register(std::string_view typeName,
+                                     CreateModelFunc createModel,
+                                     CreateViewFunc createView) {
+  m_typeMap[typeName] = Builder{std::move(createModel), std::move(createView)};
+}
+
+void NetworkTablesProvider::Show(ViewEntry* entry, Window* window) {
+  // if there's already a window, just show it
+  if (entry->window) {
+    entry->window->SetVisible(true);
+    return;
+  }
+
+  // get or create model
+  if (!entry->modelEntry->model) {
+    entry->modelEntry->model =
+        entry->modelEntry->createModel(m_nt.GetInstance(), entry->name.c_str());
+  }
+  if (!entry->modelEntry->model) {
+    return;
+  }
+
+  // the window might exist and we're just not associated to it yet
+  if (!window) {
+    window = GetOrAddWindow(entry->name, true);
+  }
+  if (!window) {
+    return;
+  }
+  if (wpi::starts_with(entry->name, "/SmartDashboard/")) {
+    window->SetDefaultName(
+        fmt::format("{} (SmartDashboard)", wpi::drop_front(entry->name, 16)));
+  }
+  entry->window = window;
+
+  // create view
+  auto view = entry->createView(window, entry->modelEntry->model.get(),
+                                entry->name.c_str());
+  if (!view) {
+    return;
+  }
+  window->SetView(std::move(view));
+
+  entry->window->SetVisible(true);
+}
+
+NetworkTablesProvider::ViewEntry* NetworkTablesProvider::GetOrCreateView(
+    const Builder& builder, NT_Entry typeEntry, std::string_view name) {
+  // get view entry if it already exists
+  auto viewIt = FindViewEntry(name);
+  if (viewIt != m_viewEntries.end() && (*viewIt)->name == name) {
+    // make sure typeEntry is set in model
+    static_cast<Entry*>((*viewIt)->modelEntry)->typeEntry = typeEntry;
+    return viewIt->get();
+  }
+
+  // get or create model entry
+  auto modelIt = FindModelEntry(name);
+  if (modelIt != m_modelEntries.end() && (*modelIt)->name == name) {
+    static_cast<Entry*>(modelIt->get())->typeEntry = typeEntry;
+  } else {
+    modelIt = m_modelEntries.emplace(
+        modelIt, std::make_unique<Entry>(typeEntry, name, builder));
+  }
+
+  // create new view entry
+  viewIt = m_viewEntries.emplace(
+      viewIt,
+      std::make_unique<ViewEntry>(
+          name, modelIt->get(), [](Model*, const char*) { return true; },
+          builder.createView));
+
+  return viewIt->get();
+}
diff --git a/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp b/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp
new file mode 100644
index 0000000..28f4de4
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp
@@ -0,0 +1,133 @@
+// 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/networktables/NetworkTablesSettings.h"
+
+#include <optional>
+#include <string_view>
+#include <utility>
+
+#include <imgui.h>
+#include <imgui_stdlib.h>
+#include <ntcore_cpp.h>
+#include <wpi/SmallVector.h>
+#include <wpi/StringExtras.h>
+
+#include "glass/Context.h"
+
+using namespace glass;
+
+void NetworkTablesSettings::Thread::Main() {
+  while (m_active) {
+    // wait to be woken up
+    std::unique_lock lock(m_mutex);
+    m_cond.wait(lock, [&] { return !m_active || m_restart; });
+    if (!m_active) {
+      break;
+    }
+
+    // clear restart flag
+    m_restart = false;
+
+    int mode;
+    bool dsClient;
+
+    do {
+      mode = m_mode;
+      dsClient = m_dsClient;
+
+      // release lock while stopping to avoid blocking GUI
+      lock.unlock();
+
+      // if just changing servers in client mode, no need to stop and restart
+      unsigned int curMode = nt::GetNetworkMode(m_inst);
+      if (mode != 1 || (curMode & NT_NET_MODE_SERVER) != 0) {
+        nt::StopClient(m_inst);
+        nt::StopServer(m_inst);
+        nt::StopLocal(m_inst);
+      }
+
+      if (m_mode != 1 || !dsClient) {
+        nt::StopDSClient(m_inst);
+      }
+
+      lock.lock();
+    } while (mode != m_mode || dsClient != m_dsClient);
+
+    if (m_mode == 1) {
+      std::string_view serverTeam{m_serverTeam};
+      std::optional<unsigned int> team;
+      if (!wpi::contains(serverTeam, '.') &&
+          (team = wpi::parse_integer<unsigned int>(serverTeam, 10))) {
+        nt::StartClientTeam(m_inst, team.value(), NT_DEFAULT_PORT);
+      } else {
+        wpi::SmallVector<std::string_view, 4> serverNames;
+        wpi::SmallVector<std::pair<std::string_view, unsigned int>, 4> servers;
+        wpi::split(serverTeam, serverNames, ',', -1, false);
+        for (auto&& serverName : serverNames) {
+          servers.emplace_back(serverName, NT_DEFAULT_PORT);
+        }
+        nt::StartClient(m_inst, servers);
+      }
+
+      if (m_dsClient) {
+        nt::StartDSClient(m_inst, NT_DEFAULT_PORT);
+      }
+    } else if (m_mode == 2) {
+      nt::StartServer(m_inst, m_iniName.c_str(), m_listenAddress.c_str(),
+                      NT_DEFAULT_PORT);
+    }
+  }
+}
+
+NetworkTablesSettings::NetworkTablesSettings(NT_Inst inst,
+                                             const char* storageName) {
+  auto& storage = glass::GetStorage(storageName);
+  m_pMode = storage.GetIntRef("mode");
+  m_pIniName = storage.GetStringRef("iniName", "networktables.ini");
+  m_pServerTeam = storage.GetStringRef("serverTeam");
+  m_pListenAddress = storage.GetStringRef("listenAddress");
+  m_pDsClient = storage.GetBoolRef("dsClient", true);
+
+  m_thread.Start(inst);
+}
+
+void NetworkTablesSettings::Update() {
+  if (!m_restart) {
+    return;
+  }
+  m_restart = false;
+
+  // do actual operation on thread
+  auto thr = m_thread.GetThread();
+  thr->m_restart = true;
+  thr->m_mode = *m_pMode;
+  thr->m_iniName = *m_pIniName;
+  thr->m_serverTeam = *m_pServerTeam;
+  thr->m_listenAddress = *m_pListenAddress;
+  thr->m_dsClient = *m_pDsClient;
+  thr->m_cond.notify_one();
+}
+
+bool NetworkTablesSettings::Display() {
+  static const char* modeOptions[] = {"Disabled", "Client", "Server"};
+  ImGui::Combo("Mode", m_pMode, modeOptions, m_serverOption ? 3 : 2);
+  switch (*m_pMode) {
+    case 1:
+      ImGui::InputText("Team/IP", m_pServerTeam);
+      ImGui::Checkbox("Get Address from DS", m_pDsClient);
+      break;
+    case 2:
+      ImGui::InputText("Listen Address", m_pListenAddress);
+      ImGui::InputText("ini Filename", m_pIniName);
+      break;
+    default:
+      break;
+  }
+  if (ImGui::Button("Apply")) {
+    m_restart = true;
+    return true;
+  }
+  return false;
+}
diff --git a/glass/src/libnt/native/cpp/StandardNetworkTables.cpp b/glass/src/libnt/native/cpp/StandardNetworkTables.cpp
new file mode 100644
index 0000000..0a5f234
--- /dev/null
+++ b/glass/src/libnt/native/cpp/StandardNetworkTables.cpp
@@ -0,0 +1,176 @@
+// 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/networktables/NTCommandScheduler.h"
+#include "glass/networktables/NTCommandSelector.h"
+#include "glass/networktables/NTDifferentialDrive.h"
+#include "glass/networktables/NTDigitalInput.h"
+#include "glass/networktables/NTDigitalOutput.h"
+#include "glass/networktables/NTFMS.h"
+#include "glass/networktables/NTField2D.h"
+#include "glass/networktables/NTGyro.h"
+#include "glass/networktables/NTMecanumDrive.h"
+#include "glass/networktables/NTMechanism2D.h"
+#include "glass/networktables/NTPIDController.h"
+#include "glass/networktables/NTSpeedController.h"
+#include "glass/networktables/NTStringChooser.h"
+#include "glass/networktables/NTSubsystem.h"
+#include "glass/networktables/NetworkTablesProvider.h"
+
+using namespace glass;
+
+void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) {
+  provider.Register(
+      NTCommandSchedulerModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTCommandSchedulerModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetDefaultSize(400, 200);
+        return MakeFunctionView([=] {
+          DisplayCommandScheduler(static_cast<NTCommandSchedulerModel*>(model));
+        });
+      });
+  provider.Register(
+      NTCommandSelectorModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTCommandSelectorModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplayCommandSelector(static_cast<NTCommandSelectorModel*>(model));
+        });
+      });
+  provider.Register(
+      NTDifferentialDriveModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTDifferentialDriveModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetDefaultSize(300, 350);
+        return MakeFunctionView([=] {
+          DisplayDrive(static_cast<NTDifferentialDriveModel*>(model));
+        });
+      });
+  provider.Register(
+      NTFMSModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTFMSModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView(
+            [=] { DisplayFMS(static_cast<FMSModel*>(model)); });
+      });
+  provider.Register(
+      NTDigitalInputModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTDigitalInputModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplayDIO(static_cast<NTDigitalInputModel*>(model), 0, true);
+        });
+      });
+  provider.Register(
+      NTDigitalOutputModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTDigitalOutputModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplayDIO(static_cast<NTDigitalOutputModel*>(model), 0, true);
+        });
+      });
+  provider.Register(
+      NTField2DModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTField2DModel>(inst, path);
+      },
+      [=](Window* win, Model* model, const char* path) {
+        win->SetDefaultPos(200, 200);
+        win->SetDefaultSize(400, 200);
+        win->SetPadding(0, 0);
+        return std::make_unique<Field2DView>(
+            static_cast<NTField2DModel*>(model));
+      });
+  provider.Register(
+      NTGyroModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTGyroModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char* path) {
+        win->SetDefaultSize(320, 380);
+        return MakeFunctionView(
+            [=] { DisplayGyro(static_cast<NTGyroModel*>(model)); });
+      });
+  provider.Register(
+      NTMecanumDriveModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTMecanumDriveModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetDefaultSize(300, 350);
+        return MakeFunctionView(
+            [=] { DisplayDrive(static_cast<NTMecanumDriveModel*>(model)); });
+      });
+  provider.Register(
+      NTMechanism2DModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTMechanism2DModel>(inst, path);
+      },
+      [=](Window* win, Model* model, const char* path) {
+        win->SetDefaultPos(400, 400);
+        win->SetDefaultSize(200, 200);
+        win->SetPadding(0, 0);
+        return std::make_unique<Mechanism2DView>(
+            static_cast<NTMechanism2DModel*>(model));
+      });
+  provider.Register(
+      NTPIDControllerModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTPIDControllerModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char* path) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplayPIDController(static_cast<NTPIDControllerModel*>(model));
+        });
+      });
+  provider.Register(
+      NTSpeedControllerModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTSpeedControllerModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char* path) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplaySpeedController(static_cast<NTSpeedControllerModel*>(model));
+        });
+      });
+  provider.Register(
+      NTStringChooserModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTStringChooserModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplayStringChooser(static_cast<NTStringChooserModel*>(model));
+        });
+      });
+  provider.Register(
+      NTSubsystemModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTSubsystemModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView(
+            [=] { DisplaySubsystem(static_cast<NTSubsystemModel*>(model)); });
+      });
+}
diff --git a/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h b/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h
new file mode 100644
index 0000000..54dc778
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h
@@ -0,0 +1,47 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/CommandScheduler.h"
+
+namespace glass {
+class NTCommandSchedulerModel : public CommandSchedulerModel {
+ public:
+  static constexpr const char* kType = "Scheduler";
+
+  explicit NTCommandSchedulerModel(std::string_view path);
+  NTCommandSchedulerModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const std::vector<std::string>& GetCurrentCommands() override {
+    return m_commandsValue;
+  }
+
+  void CancelCommand(size_t index) override;
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return false; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_name;
+  NT_Entry m_commands;
+  NT_Entry m_ids;
+  NT_Entry m_cancel;
+
+  std::string m_nameValue;
+  std::vector<std::string> m_commandsValue;
+  std::vector<double> m_idsValue;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h b/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h
new file mode 100644
index 0000000..c936665
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h
@@ -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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/CommandSelector.h"
+
+namespace glass {
+class NTCommandSelectorModel : public CommandSelectorModel {
+ public:
+  static constexpr const char* kType = "Command";
+
+  explicit NTCommandSelectorModel(std::string_view path);
+  NTCommandSelectorModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  DataSource* GetRunningData() override { return &m_runningData; }
+  void SetRunning(bool run) override;
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return false; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_running;
+  NT_Entry m_name;
+
+  DataSource m_runningData;
+  std::string m_nameValue;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h b/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h
new file mode 100644
index 0000000..49b3eb0
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h
@@ -0,0 +1,53 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/Drive.h"
+
+namespace glass {
+class NTDifferentialDriveModel : public DriveModel {
+ public:
+  static constexpr const char* kType = "DifferentialDrive";
+
+  explicit NTDifferentialDriveModel(std::string_view path);
+  NTDifferentialDriveModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const std::vector<DriveModel::WheelInfo>& GetWheels() const override {
+    return m_wheels;
+  }
+
+  ImVec2 GetSpeedVector() const override { return m_speedVector; }
+  double GetRotation() const override { return m_rotation; }
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return !m_controllableValue; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_name;
+  NT_Entry m_controllable;
+  NT_Entry m_lPercent;
+  NT_Entry m_rPercent;
+
+  std::string m_nameValue;
+  bool m_controllableValue = false;
+  DataSource m_lPercentData;
+  DataSource m_rPercentData;
+
+  std::vector<DriveModel::WheelInfo> m_wheels;
+  ImVec2 m_speedVector;
+  double m_rotation;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h b/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h
new file mode 100644
index 0000000..cd3dfeb
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h
@@ -0,0 +1,53 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/hardware/DIO.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+
+namespace glass {
+
+class NTDigitalInputModel : public DIOModel {
+ public:
+  static constexpr const char* kType = "Digital Input";
+
+  // path is to the table containing ".type", excluding the trailing /
+  explicit NTDigitalInputModel(std::string_view path);
+  NTDigitalInputModel(NT_Inst inst, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+
+  const char* GetSimDevice() const override { return nullptr; }
+
+  DPWMModel* GetDPWM() override { return nullptr; }
+  DutyCycleModel* GetDutyCycle() override { return nullptr; }
+  EncoderModel* GetEncoder() override { return nullptr; }
+
+  bool IsInput() const override { return true; }
+
+  DataSource* GetValueData() override { return &m_valueData; }
+
+  void SetValue(bool val) override {}
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return true; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_value;
+  NT_Entry m_name;
+
+  DataSource m_valueData;
+  std::string m_nameValue;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h b/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h
new file mode 100644
index 0000000..8ed1ee7
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h
@@ -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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/hardware/DIO.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+
+namespace glass {
+
+class NTDigitalOutputModel : public DIOModel {
+ public:
+  static constexpr const char* kType = "Digital Output";
+
+  // path is to the table containing ".type", excluding the trailing /
+  explicit NTDigitalOutputModel(std::string_view path);
+  NTDigitalOutputModel(NT_Inst inst, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+
+  const char* GetSimDevice() const override { return nullptr; }
+
+  DPWMModel* GetDPWM() override { return nullptr; }
+  DutyCycleModel* GetDutyCycle() override { return nullptr; }
+  EncoderModel* GetEncoder() override { return nullptr; }
+
+  bool IsInput() const override { return true; }
+
+  DataSource* GetValueData() override { return &m_valueData; }
+
+  void SetValue(bool val) override;
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return !m_controllableValue; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_value;
+  NT_Entry m_name;
+  NT_Entry m_controllable;
+
+  DataSource m_valueData;
+  std::string m_nameValue;
+  bool m_controllableValue = false;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTFMS.h b/glass/src/libnt/native/include/glass/networktables/NTFMS.h
new file mode 100644
index 0000000..b19a9f0
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTFMS.h
@@ -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.
+
+#pragma once
+
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/FMS.h"
+
+namespace glass {
+
+class NTFMSModel : public FMSModel {
+ public:
+  static constexpr const char* kType = "FMSInfo";
+
+  // path is to the table containing ".type", excluding the trailing /
+  explicit NTFMSModel(std::string_view path);
+  NTFMSModel(NT_Inst inst, std::string_view path);
+
+  DataSource* GetFmsAttachedData() override { return &m_fmsAttached; }
+  DataSource* GetDsAttachedData() override { return &m_dsAttached; }
+  DataSource* GetAllianceStationIdData() override {
+    return &m_allianceStationId;
+  }
+  // NT does not provide match time
+  DataSource* GetMatchTimeData() override { return nullptr; }
+  DataSource* GetEStopData() override { return &m_estop; }
+  DataSource* GetEnabledData() override { return &m_enabled; }
+  DataSource* GetTestData() override { return &m_test; }
+  DataSource* GetAutonomousData() override { return &m_autonomous; }
+  std::string_view GetGameSpecificMessage(
+      wpi::SmallVectorImpl<char>& buf) override;
+
+  // NT is read-only (it's continually set by robot code)
+  void SetFmsAttached(bool val) override {}
+  void SetDsAttached(bool val) override {}
+  void SetAllianceStationId(int val) override {}
+  void SetMatchTime(double val) override {}
+  void SetEStop(bool val) override {}
+  void SetEnabled(bool val) override {}
+  void SetTest(bool val) override {}
+  void SetAutonomous(bool val) override {}
+  void SetGameSpecificMessage(const char* val) override {}
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return true; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_gameSpecificMessage;
+  NT_Entry m_alliance;
+  NT_Entry m_station;
+  NT_Entry m_controlWord;
+
+  DataSource m_fmsAttached;
+  DataSource m_dsAttached;
+  DataSource m_allianceStationId;
+  DataSource m_estop;
+  DataSource m_enabled;
+  DataSource m_test;
+  DataSource m_autonomous;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTField2D.h b/glass/src/libnt/native/include/glass/networktables/NTField2D.h
new file mode 100644
index 0000000..f966e0f
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTField2D.h
@@ -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.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/Field2D.h"
+
+namespace glass {
+
+class NTField2DModel : public Field2DModel {
+ public:
+  static constexpr const char* kType = "Field2d";
+
+  // path is to the table containing ".type", excluding the trailing /
+  explicit NTField2DModel(std::string_view path);
+  NTField2DModel(NT_Inst inst, std::string_view path);
+  ~NTField2DModel() override;
+
+  const char* GetPath() const { return m_path.c_str(); }
+  const char* GetName() const { return m_nameValue.c_str(); }
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override;
+
+  FieldObjectModel* AddFieldObject(std::string_view name) override;
+  void RemoveFieldObject(std::string_view name) override;
+  void ForEachFieldObject(
+      wpi::function_ref<void(FieldObjectModel& model, std::string_view name)>
+          func) override;
+
+ private:
+  NetworkTablesHelper m_nt;
+  std::string m_path;
+  NT_Entry m_name;
+  std::string m_nameValue;
+
+  class ObjectModel;
+  using Objects = std::vector<std::unique_ptr<ObjectModel>>;
+  Objects m_objects;
+
+  std::pair<Objects::iterator, bool> Find(std::string_view fullName);
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTGyro.h b/glass/src/libnt/native/include/glass/networktables/NTGyro.h
new file mode 100644
index 0000000..db303a5
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTGyro.h
@@ -0,0 +1,42 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/hardware/Gyro.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+
+namespace glass {
+class NTGyroModel : public GyroModel {
+ public:
+  static constexpr const char* kType = "Gyro";
+
+  explicit NTGyroModel(std::string_view path);
+  NTGyroModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const char* GetSimDevice() const override { return nullptr; }
+
+  DataSource* GetAngleData() override { return &m_angleData; }
+  void SetAngle(double value) override {}
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return true; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_angle;
+  NT_Entry m_name;
+
+  DataSource m_angleData;
+  std::string m_nameValue;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h b/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h
new file mode 100644
index 0000000..ce7d234
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h
@@ -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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/Drive.h"
+
+namespace glass {
+class NTMecanumDriveModel : public DriveModel {
+ public:
+  static constexpr const char* kType = "MecanumDrive";
+
+  explicit NTMecanumDriveModel(std::string_view path);
+  NTMecanumDriveModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const std::vector<DriveModel::WheelInfo>& GetWheels() const override {
+    return m_wheels;
+  }
+
+  ImVec2 GetSpeedVector() const override { return m_speedVector; }
+  double GetRotation() const override { return m_rotation; }
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return !m_controllableValue; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_name;
+  NT_Entry m_controllable;
+  NT_Entry m_flPercent;
+  NT_Entry m_frPercent;
+  NT_Entry m_rlPercent;
+  NT_Entry m_rrPercent;
+
+  std::string m_nameValue;
+  bool m_controllableValue = false;
+  DataSource m_flPercentData;
+  DataSource m_frPercentData;
+  DataSource m_rlPercentData;
+  DataSource m_rrPercentData;
+
+  std::vector<DriveModel::WheelInfo> m_wheels;
+  ImVec2 m_speedVector;
+  double m_rotation;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h b/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h
new file mode 100644
index 0000000..81f7df1
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h
@@ -0,0 +1,60 @@
+// 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.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include <frc/geometry/Translation2d.h>
+#include <ntcore_cpp.h>
+
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/Mechanism2D.h"
+
+namespace glass {
+
+class NTMechanism2DModel : public Mechanism2DModel {
+ public:
+  static constexpr const char* kType = "Mechanism2d";
+
+  // path is to the table containing ".type", excluding the trailing /
+  explicit NTMechanism2DModel(std::string_view path);
+  NTMechanism2DModel(NT_Inst inst, std::string_view path);
+  ~NTMechanism2DModel() override;
+
+  const char* GetPath() const { return m_path.c_str(); }
+  const char* GetName() const { return m_nameValue.c_str(); }
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override;
+
+  frc::Translation2d GetDimensions() const override {
+    return m_dimensionsValue;
+  }
+  ImU32 GetBackgroundColor() const override { return m_bgColorValue; }
+  void ForEachRoot(
+      wpi::function_ref<void(MechanismRootModel& model)> func) override;
+
+ private:
+  NetworkTablesHelper m_nt;
+  std::string m_path;
+
+  NT_Entry m_name;
+  NT_Entry m_dimensions;
+  NT_Entry m_bgColor;
+
+  std::string m_nameValue;
+  frc::Translation2d m_dimensionsValue;
+  ImU32 m_bgColorValue = 0;
+
+  class RootModel;
+  std::vector<std::unique_ptr<RootModel>> m_roots;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTPIDController.h b/glass/src/libnt/native/include/glass/networktables/NTPIDController.h
new file mode 100644
index 0000000..b975641
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTPIDController.h
@@ -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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/PIDController.h"
+
+namespace glass {
+class NTPIDControllerModel : public PIDControllerModel {
+ public:
+  static constexpr const char* kType = "PIDController";
+
+  explicit NTPIDControllerModel(std::string_view path);
+  NTPIDControllerModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+
+  DataSource* GetPData() override { return &m_pData; }
+  DataSource* GetIData() override { return &m_iData; }
+  DataSource* GetDData() override { return &m_dData; }
+  DataSource* GetSetpointData() override { return &m_setpointData; }
+
+  void SetP(double value) override;
+  void SetI(double value) override;
+  void SetD(double value) override;
+  void SetSetpoint(double value) override;
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return !m_controllableValue; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_name;
+  NT_Entry m_controllable;
+  NT_Entry m_p;
+  NT_Entry m_i;
+  NT_Entry m_d;
+  NT_Entry m_setpoint;
+
+  DataSource m_pData;
+  DataSource m_iData;
+  DataSource m_dData;
+  DataSource m_setpointData;
+
+  std::string m_nameValue;
+  bool m_controllableValue = false;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h b/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h
new file mode 100644
index 0000000..756d947
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h
@@ -0,0 +1,44 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/hardware/SpeedController.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+
+namespace glass {
+class NTSpeedControllerModel : public SpeedControllerModel {
+ public:
+  static constexpr const char* kType = "Speed Controller";
+
+  explicit NTSpeedControllerModel(std::string_view path);
+  NTSpeedControllerModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const char* GetSimDevice() const override { return nullptr; }
+
+  DataSource* GetPercentData() override { return &m_valueData; }
+  void SetPercent(double value) override;
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return !m_controllableValue; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_value;
+  NT_Entry m_name;
+  NT_Entry m_controllable;
+
+  DataSource m_valueData;
+  std::string m_nameValue;
+  bool m_controllableValue = false;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h b/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h
new file mode 100644
index 0000000..2d806c9
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h
@@ -0,0 +1,54 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/StringChooser.h"
+
+namespace glass {
+
+class NTStringChooserModel : public StringChooserModel {
+ public:
+  static constexpr const char* kType = "String Chooser";
+
+  // path is to the table containing ".type", excluding the trailing /
+  explicit NTStringChooserModel(std::string_view path);
+  NTStringChooserModel(NT_Inst inst, std::string_view path);
+
+  const std::string& GetDefault() override { return m_defaultValue; }
+  const std::string& GetSelected() override { return m_selectedValue; }
+  const std::string& GetActive() override { return m_activeValue; }
+  const std::vector<std::string>& GetOptions() override {
+    return m_optionsValue;
+  }
+
+  void SetDefault(std::string_view val) override;
+  void SetSelected(std::string_view val) override;
+  void SetActive(std::string_view val) override;
+  void SetOptions(wpi::span<const std::string> val) override;
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return false; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_default;
+  NT_Entry m_selected;
+  NT_Entry m_active;
+  NT_Entry m_options;
+
+  std::string m_defaultValue;
+  std::string m_selectedValue;
+  std::string m_activeValue;
+  std::vector<std::string> m_optionsValue;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h b/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h
new file mode 100644
index 0000000..c5862cf
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h
@@ -0,0 +1,46 @@
+// 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.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/Subsystem.h"
+
+namespace glass {
+class NTSubsystemModel : public SubsystemModel {
+ public:
+  static constexpr const char* kType = "Subsystem";
+
+  explicit NTSubsystemModel(std::string_view path);
+  NTSubsystemModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const char* GetDefaultCommand() const override {
+    return m_defaultCommandValue.c_str();
+  }
+  const char* GetCurrentCommand() const override {
+    return m_currentCommandValue.c_str();
+  }
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return true; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_name;
+  NT_Entry m_defaultCommand;
+  NT_Entry m_currentCommand;
+
+  std::string m_nameValue;
+  std::string m_defaultCommandValue;
+  std::string m_currentCommandValue;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h
new file mode 100644
index 0000000..8538eaa
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h
@@ -0,0 +1,141 @@
+// 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.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+#include <wpi/DenseMap.h>
+
+#include "glass/Model.h"
+#include "glass/View.h"
+
+namespace glass {
+
+class DataSource;
+
+class NetworkTablesModel : public Model {
+ public:
+  struct Entry {
+    explicit Entry(nt::EntryNotification&& event);
+
+    void UpdateValue();
+
+    /** Entry handle. */
+    NT_Entry entry;
+
+    /** Entry name. */
+    std::string name;
+
+    /** The value. */
+    std::shared_ptr<nt::Value> value;
+
+    /** Flags. */
+    unsigned int flags = 0;
+
+    /** String representation of the value (for arrays / complex values). */
+    std::string valueStr;
+
+    /** Data source (for numeric values). */
+    std::unique_ptr<DataSource> source;
+  };
+
+  struct TreeNode {
+    explicit TreeNode(std::string_view name) : name{name} {}
+
+    /** Short name (e.g. of just this node) */
+    std::string name;
+
+    /**
+     * Full path if entry is null (otherwise use entry->name),
+     * excluding trailing /
+     */
+    std::string path;
+
+    /** Null if no value at this node */
+    Entry* entry = nullptr;
+
+    /** Children of node, sorted by name */
+    std::vector<TreeNode> children;
+  };
+
+  NetworkTablesModel();
+  explicit NetworkTablesModel(NT_Inst inst);
+  ~NetworkTablesModel() override;
+
+  void Update() override;
+  bool Exists() override;
+
+  NT_Inst GetInstance() { return m_inst; }
+  const std::vector<Entry*>& GetEntries() { return m_sortedEntries; }
+  const std::vector<TreeNode>& GetTreeRoot() { return m_root; }
+
+ private:
+  NT_Inst m_inst;
+  NT_EntryListenerPoller m_poller;
+  wpi::DenseMap<NT_Entry, std::unique_ptr<Entry>> m_entries;
+
+  // sorted by name
+  std::vector<Entry*> m_sortedEntries;
+
+  std::vector<TreeNode> m_root;
+};
+
+using NetworkTablesFlags = int;
+
+enum NetworkTablesFlags_ {
+  NetworkTablesFlags_TreeView = 1 << 0,
+  NetworkTablesFlags_ReadOnly = 1 << 1,
+  NetworkTablesFlags_ShowConnections = 1 << 2,
+  NetworkTablesFlags_ShowFlags = 1 << 3,
+  NetworkTablesFlags_ShowTimestamp = 1 << 4,
+  NetworkTablesFlags_CreateNoncanonicalKeys = 1 << 5,
+  NetworkTablesFlags_Default = 1 & ~NetworkTablesFlags_ReadOnly &
+                               ~NetworkTablesFlags_CreateNoncanonicalKeys
+};
+
+void DisplayNetworkTables(
+    NetworkTablesModel* model,
+    NetworkTablesFlags flags = NetworkTablesFlags_Default);
+
+class NetworkTablesFlagsSettings {
+ public:
+  explicit NetworkTablesFlagsSettings(
+      NetworkTablesFlags defaultFlags = NetworkTablesFlags_Default)
+      : m_defaultFlags{defaultFlags}, m_flags{defaultFlags} {}
+
+  void Update();
+  void DisplayMenu();
+
+  NetworkTablesFlags GetFlags() const { return m_flags; }
+
+ private:
+  bool* m_pTreeView = nullptr;
+  bool* m_pShowConnections = nullptr;
+  bool* m_pShowFlags = nullptr;
+  bool* m_pShowTimestamp = nullptr;
+  bool* m_pCreateNoncanonicalKeys = nullptr;
+  NetworkTablesFlags m_defaultFlags;  // NOLINT
+  NetworkTablesFlags m_flags;         // NOLINT
+};
+
+class NetworkTablesView : public View {
+ public:
+  explicit NetworkTablesView(
+      NetworkTablesModel* model,
+      NetworkTablesFlags defaultFlags = NetworkTablesFlags_Default)
+      : m_model{model}, m_flags{defaultFlags} {}
+
+  void Display() override;
+
+ private:
+  NetworkTablesModel* m_model;
+  NetworkTablesFlagsSettings m_flags;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h
new file mode 100644
index 0000000..aba3252
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h
@@ -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.
+
+#pragma once
+
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+namespace glass {
+
+class NetworkTablesHelper {
+ public:
+  explicit NetworkTablesHelper(NT_Inst inst);
+  ~NetworkTablesHelper();
+
+  NetworkTablesHelper(const NetworkTablesHelper&) = delete;
+  NetworkTablesHelper& operator=(const NetworkTablesHelper&) = delete;
+
+  NT_Inst GetInstance() const { return m_inst; }
+  NT_EntryListenerPoller GetPoller() const { return m_poller; }
+
+  NT_Entry GetEntry(std::string_view name) const {
+    return nt::GetEntry(m_inst, name);
+  }
+
+  static constexpr int kDefaultListenerFlags =
+      NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE | NT_NOTIFY_DELETE |
+      NT_NOTIFY_IMMEDIATE;
+
+  NT_EntryListener AddListener(NT_Entry entry,
+                               unsigned int flags = kDefaultListenerFlags) {
+    return nt::AddPolledEntryListener(m_poller, entry, flags);
+  }
+
+  NT_EntryListener AddListener(std::string_view prefix,
+                               unsigned int flags = kDefaultListenerFlags) {
+    return nt::AddPolledEntryListener(m_poller, prefix, flags);
+  }
+
+  std::vector<nt::EntryNotification> PollListener() {
+    bool timedOut = false;
+    return nt::PollEntryListener(m_poller, 0, &timedOut);
+  }
+
+  bool IsConnected() const;
+
+ private:
+  NT_Inst m_inst;
+  NT_EntryListenerPoller m_poller;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h
new file mode 100644
index 0000000..17374ca
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h
@@ -0,0 +1,114 @@
+// 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.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+#include <wpi/StringMap.h>
+
+#include "glass/Model.h"
+#include "glass/Provider.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/support/IniSaverInfo.h"
+#include "glass/support/IniSaverString.h"
+
+namespace glass {
+
+class Window;
+
+namespace detail {
+struct NTProviderFunctions {
+  using Exists = std::function<bool(NT_Inst inst, const char* path)>;
+  using CreateModel =
+      std::function<std::unique_ptr<Model>(NT_Inst inst, const char* path)>;
+  using ViewExists = std::function<bool(Model*, const char* path)>;
+  using CreateView =
+      std::function<std::unique_ptr<View>(Window*, Model*, const char* path)>;
+};
+}  // namespace detail
+
+/**
+ * A provider for NetworkTables (SmartDashboard style) models and views.
+ */
+class NetworkTablesProvider : private Provider<detail::NTProviderFunctions> {
+ public:
+  using Provider::CreateModelFunc;
+  using Provider::CreateViewFunc;
+
+  explicit NetworkTablesProvider(std::string_view iniName);
+  NetworkTablesProvider(std::string_view iniName, NT_Inst inst);
+
+  /**
+   * Get the NetworkTables instance being used for this provider.
+   *
+   * @return NetworkTables instance
+   */
+  NT_Inst GetInstance() const { return m_nt.GetInstance(); }
+
+  /**
+   * Perform global initialization.  This should be called prior to
+   * wpi::gui::Initialize().
+   */
+  void GlobalInit() override;
+
+  /**
+   * Displays menu contents as a tree of available NetworkTables views.
+   */
+  void DisplayMenu() override;
+
+  /**
+   * Registers a NetworkTables model and view.
+   *
+   * @param typeName SmartDashboard .type value to match
+   * @param createModel functor to create model
+   * @param createView functor to create view
+   */
+  void Register(std::string_view typeName, CreateModelFunc createModel,
+                CreateViewFunc createView);
+
+  using WindowManager::AddWindow;
+
+ private:
+  void Update() override;
+
+  NetworkTablesHelper m_nt;
+
+  // cached mapping from table name to type string
+  IniSaverString<NameInfo> m_typeCache;
+
+  struct Builder {
+    CreateModelFunc createModel;
+    CreateViewFunc createView;
+  };
+
+  // mapping from .type string to model/view creators
+  wpi::StringMap<Builder> m_typeMap;
+
+  struct Entry : public ModelEntry {
+    Entry(NT_Entry typeEntry, std::string_view name, const Builder& builder)
+        : ModelEntry{name, [](NT_Inst, const char*) { return true; },
+                     builder.createModel},
+          typeEntry{typeEntry} {}
+    NT_Entry typeEntry;
+  };
+
+  void Show(ViewEntry* entry, Window* window) override;
+
+  ViewEntry* GetOrCreateView(const Builder& builder, NT_Entry typeEntry,
+                             std::string_view name);
+};
+
+/**
+ * Add "standard" set of NetworkTables models/views.
+ *
+ * @param provider NetworkTables provider
+ */
+void AddStandardNetworkTablesViews(NetworkTablesProvider& provider);
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h
new file mode 100644
index 0000000..7738541
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h
@@ -0,0 +1,59 @@
+// 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.
+
+#pragma once
+
+#include <string>
+
+#include <ntcore_cpp.h>
+#include <wpi/SafeThread.h>
+
+namespace wpi {
+template <typename T>
+class SmallVectorImpl;
+}  // namespace wpi
+
+namespace glass {
+
+class NetworkTablesSettings {
+ public:
+  explicit NetworkTablesSettings(
+      NT_Inst inst = nt::GetDefaultInstance(),
+      const char* storageName = "NetworkTables Settings");
+
+  /**
+   * Enables or disables the server option.  Default is enabled.
+   */
+  void EnableServerOption(bool enable) { m_serverOption = enable; }
+
+  void Update();
+  bool Display();
+
+ private:
+  bool m_restart = true;
+  bool m_serverOption = true;
+  int* m_pMode;
+  std::string* m_pIniName;
+  std::string* m_pServerTeam;
+  std::string* m_pListenAddress;
+  bool* m_pDsClient;
+
+  class Thread : public wpi::SafeThread {
+   public:
+    explicit Thread(NT_Inst inst) : m_inst{inst} {}
+
+    void Main() override;
+
+    NT_Inst m_inst;
+    bool m_restart = false;
+    int m_mode;
+    std::string m_iniName;
+    std::string m_serverTeam;
+    std::string m_listenAddress;
+    bool m_dsClient;
+  };
+  wpi::SafeThreadOwner<Thread> m_thread;
+};
+
+}  // namespace glass