1+ use bevy:: prelude:: Entity ;
2+ use processing:: prelude:: * ;
3+ use pyo3:: exceptions:: PyRuntimeError ;
4+ use pyo3:: prelude:: * ;
5+ use pyo3:: types:: PyAny ;
6+ use std:: cell:: RefCell ;
7+
8+ use crate :: glfw:: GlfwContext ;
9+
10+ // The global graphics context
11+ // we use thread_local! to ensure that the context is specific to the current thread, particularly
12+ // becausae glfw requires that all window operations happen on the main thread.
13+ // TODO: we'll want to think about how to enable multi-window / multi-threaded usage in the future
14+ thread_local ! {
15+ static GRAPHICS_CTX : RefCell <Option <Py <Graphics >>> = const { RefCell :: new( None ) } ;
16+ }
17+
18+ #[ pyclass( unsendable) ]
19+ pub struct Graphics {
20+ // this makes our object !Send, hence unsendable above
21+ glfw_ctx : GlfwContext ,
22+ surface : Entity ,
23+ }
24+
25+ #[ pymethods]
26+ impl Graphics {
27+ // TODO: in theory users can create multiple Graphics objects, which they manually manage themselves.
28+ // right now we just support the single global window via the module-level functions.
29+ #[ new]
30+ fn new ( width : u32 , height : u32 ) -> PyResult < Self > {
31+ let glfw_ctx = GlfwContext :: new ( width, height)
32+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Couold not create window {e}" ) ) ) ?;
33+
34+ init ( ) . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to initialize processing {e}" ) ) ) ?;
35+
36+ let window_handle = glfw_ctx. get_window ( ) ;
37+ let display_handle = glfw_ctx. get_display ( ) ;
38+ let surface = surface_create ( window_handle, display_handle, width, height, 1.0 )
39+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Could not create surface {e}" ) ) ) ?;
40+
41+ Ok ( Self { glfw_ctx, surface } )
42+ }
43+
44+ pub fn background ( & self , args : Vec < f32 > ) -> PyResult < ( ) > {
45+ let ( r, g, b, a) = parse_color ( & args) ?;
46+ let color = bevy:: color:: Color :: srgba ( r, g, b, a) ;
47+ record_command ( self . surface , DrawCommand :: BackgroundColor ( color) )
48+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "background failed {e}" ) ) )
49+ }
50+
51+ pub fn fill ( & self , args : Vec < f32 > ) -> PyResult < ( ) > {
52+ let ( r, g, b, a) = parse_color ( & args) ?;
53+ let color = bevy:: color:: Color :: srgba ( r, g, b, a) ;
54+ record_command ( self . surface , DrawCommand :: Fill ( color) )
55+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "fill failed {e}" ) ) )
56+ }
57+
58+ pub fn no_fill ( & self ) -> PyResult < ( ) > {
59+ record_command ( self . surface , DrawCommand :: NoFill )
60+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "no_fill failed {e}" ) ) )
61+ }
62+
63+ pub fn stroke ( & self , args : Vec < f32 > ) -> PyResult < ( ) > {
64+ let ( r, g, b, a) = parse_color ( & args) ?;
65+ let color = bevy:: color:: Color :: srgba ( r, g, b, a) ;
66+ record_command ( self . surface , DrawCommand :: StrokeColor ( color) )
67+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "stroke failed {e}" ) ) )
68+ }
69+
70+ pub fn no_stroke ( & self ) -> PyResult < ( ) > {
71+ record_command ( self . surface , DrawCommand :: NoStroke )
72+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "no_stroke failed {e}" ) ) )
73+ }
74+
75+ pub fn stroke_weight ( & self , weight : f32 ) -> PyResult < ( ) > {
76+ record_command ( self . surface , DrawCommand :: StrokeWeight ( weight) )
77+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "stroke_weight failed {e}" ) ) )
78+ }
79+
80+ pub fn rect ( & self , x : f32 , y : f32 , w : f32 , h : f32 , tl : f32 , tr : f32 , br : f32 , bl : f32 ) -> PyResult < ( ) > {
81+ record_command (
82+ self . surface ,
83+ DrawCommand :: Rect {
84+ x,
85+ y,
86+ w,
87+ h,
88+ radii : [ tl, tr, br, bl] ,
89+ } ,
90+ )
91+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "rect failed {e}" ) ) )
92+ }
93+
94+ pub fn run ( & mut self , draw_fn : Option < Py < PyAny > > ) -> PyResult < ( ) > {
95+ loop {
96+ let running = self . glfw_ctx . poll_events ( ) ;
97+ if !running {
98+ break ;
99+ }
100+
101+ begin_draw ( self . surface )
102+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "begin_draw failed {e}" ) ) ) ?;
103+
104+ if let Some ( ref draw) = draw_fn {
105+ Python :: attach ( |py| {
106+ draw. call0 ( py)
107+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "draw failed {e}" ) ) )
108+ } ) ?;
109+ }
110+
111+ end_draw ( self . surface )
112+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "end_draw failed {e}" ) ) ) ?;
113+ }
114+
115+ Ok ( ( ) )
116+ }
117+ }
118+
119+ // TODO: a real color type. or color parser? idk. color is confusing. let's think
120+ // about how to expose different color spaces in an idiomatic pythonic way
121+ fn parse_color ( args : & [ f32 ] ) -> PyResult < ( f32 , f32 , f32 , f32 ) > {
122+ match args. len ( ) {
123+ 4 => Ok ( (
124+ args[ 0 ] / 255.0 ,
125+ args[ 1 ] / 255.0 ,
126+ args[ 2 ] / 255.0 ,
127+ args[ 3 ] / 255.0 ,
128+ ) ) ,
129+ _ => Err ( PyRuntimeError :: new_err (
130+ "Color requires 4 arguments" ,
131+ ) ) ,
132+ }
133+ }
134+
135+ /// Run inside the current graphics context
136+ pub fn with_graphics < F , T > ( f : F ) -> PyResult < T >
137+ where
138+ F : FnOnce ( PyRef < ' _ , Graphics > ) -> PyResult < T > ,
139+ {
140+ GRAPHICS_CTX . with ( |cell| {
141+ let opt = cell. borrow ( ) ;
142+ match opt. as_ref ( ) {
143+ Some ( py_graphics) => Python :: attach ( |py| {
144+ let graphics = py_graphics. bind ( py) . borrow ( ) ;
145+ f ( graphics)
146+ } ) ,
147+ None => Err ( PyRuntimeError :: new_err (
148+ "No graphics context" ,
149+ ) ) ,
150+ }
151+ } )
152+ }
153+
154+ /// Run inside the current graphics context with mutable access
155+ pub fn with_graphics_mut < F , T > ( f : F ) -> PyResult < T >
156+ where
157+ F : FnOnce ( PyRefMut < ' _ , Graphics > ) -> PyResult < T > ,
158+ {
159+ GRAPHICS_CTX . with ( |cell| {
160+ let opt = cell. borrow ( ) ;
161+ match opt. as_ref ( ) {
162+ Some ( py_graphics) => Python :: attach ( |py| {
163+ let graphics = py_graphics. bind ( py) . borrow_mut ( ) ;
164+ f ( graphics)
165+ } ) ,
166+ None => Err ( PyRuntimeError :: new_err (
167+ "No graphics context" ,
168+ ) ) ,
169+ }
170+ } )
171+ }
172+
173+ /// Create the module level graphics context
174+ pub fn create_context ( width : u32 , height : u32 ) -> PyResult < ( ) > {
175+ let already_exists = GRAPHICS_CTX . with ( |cell| cell. borrow ( ) . is_some ( ) ) ;
176+ if already_exists {
177+ return Err ( PyRuntimeError :: new_err ( "A context already exists" ) ) ;
178+ }
179+
180+ Python :: attach ( |py| {
181+ let graphics = Py :: new ( py, Graphics :: new ( width, height) ?) ?;
182+ GRAPHICS_CTX . with ( |cell| {
183+ * cell. borrow_mut ( ) = Some ( graphics) ;
184+ } ) ;
185+ Ok ( ( ) )
186+ } )
187+ }
0 commit comments