termux_gui/components/
layout.rs

1//! Layout components
2
3use serde_json::json;
4use crate::activity::Activity;
5use crate::view::View;
6use crate::error::Result;
7
8/// A LinearLayout arranges views linearly
9pub struct LinearLayout {
10    view: View,
11    #[allow(dead_code)]
12    aid: i64,
13}
14
15impl LinearLayout {
16    /// Create a new vertical LinearLayout
17    pub fn new(activity: &mut Activity, parent: Option<i64>) -> Result<Self> {
18        Self::new_with_orientation(activity, parent, true)
19    }
20    
21    /// Create a new LinearLayout with specified orientation
22    /// 
23    /// # Arguments
24    /// * `vertical` - If true, arranges children vertically; if false, horizontally
25    pub fn new_with_orientation(activity: &mut Activity, parent: Option<i64>, vertical: bool) -> Result<Self> {
26        let mut params = json!({
27            "aid": activity.id(),
28            "vertical": vertical
29        });
30        
31        // Only set parent if explicitly provided
32        if let Some(parent_id) = parent {
33            params["parent"] = json!(parent_id);
34        }
35        
36        let response = activity.send_read(&json!({
37            "method": "createLinearLayout",
38            "params": params
39        }))?;
40        
41        let id = response
42            .as_i64()
43            .ok_or_else(|| crate::error::GuiError::InvalidResponse("Invalid id".to_string()))?;
44        
45        Ok(LinearLayout {
46            view: View::new(id),
47            aid: activity.id(),
48        })
49    }
50    
51    /// Get the view ID
52    pub fn id(&self) -> i64 {
53        self.view.id()
54    }
55    
56    /// Get the underlying View
57    pub fn view(&self) -> &View {
58        &self.view
59    }
60}
61
62/// A NestedScrollView provides scrolling capability
63pub struct NestedScrollView {
64    view: View,
65    #[allow(dead_code)]
66    aid: i64,
67}
68
69impl NestedScrollView {
70    /// Create a new NestedScrollView
71    pub fn new(activity: &mut Activity, parent: Option<i64>) -> Result<Self> {
72        let mut params = json!({
73            "aid": activity.id(),
74            "nobar": false,
75            "snapping": false
76        });
77        
78        // Only set parent if explicitly provided
79        if let Some(parent_id) = parent {
80            params["parent"] = json!(parent_id);
81        }
82        
83        let response = activity.send_read(&json!({
84            "method": "createNestedScrollView",
85            "params": params
86        }))?;
87        
88        let id = response
89            .as_i64()
90            .ok_or_else(|| crate::error::GuiError::InvalidResponse("Invalid id".to_string()))?;
91        
92        Ok(NestedScrollView {
93            view: View::new(id),
94            aid: activity.id(),
95        })
96    }
97    
98    /// Get the view ID
99    pub fn id(&self) -> i64 {
100        self.view.id()
101    }
102    
103    /// Get the underlying View
104    pub fn view(&self) -> &View {
105        &self.view
106    }
107}
108
109/// A FrameLayout is a simple layout that stacks children on top of each other
110pub struct FrameLayout {
111    view: View,
112    #[allow(dead_code)]
113    aid: i64,
114}
115
116impl FrameLayout {
117    /// Create a new FrameLayout
118    /// 
119    /// Children are drawn in the order they are added, with the last child on top.
120    /// FrameLayout is useful for overlaying views or creating simple stacked layouts.
121    pub fn new(activity: &mut Activity, parent: Option<i64>) -> Result<Self> {
122        let mut params = json!({
123            "aid": activity.id()
124        });
125        
126        // Only set parent if explicitly provided
127        if let Some(parent_id) = parent {
128            params["parent"] = json!(parent_id);
129        }
130        
131        let response = activity.send_read(&json!({
132            "method": "createFrameLayout",
133            "params": params
134        }))?;
135        
136        let id = response
137            .as_i64()
138            .ok_or_else(|| crate::error::GuiError::InvalidResponse("Invalid id".to_string()))?;
139        
140        Ok(FrameLayout {
141            view: View::new(id),
142            aid: activity.id(),
143        })
144    }
145    
146    /// Get the view ID
147    pub fn id(&self) -> i64 {
148        self.view.id()
149    }
150    
151    /// Get the underlying View
152    pub fn view(&self) -> &View {
153        &self.view
154    }
155}
156
157/// A GridLayout arranges children in a grid
158pub struct GridLayout {
159    view: View,
160    #[allow(dead_code)]
161    aid: i64,
162    #[allow(dead_code)]
163    rows: i32,
164    #[allow(dead_code)]
165    cols: i32,
166}
167
168impl GridLayout {
169    /// Create a new GridLayout with specified rows and columns
170    /// 
171    /// # Arguments
172    /// * `rows` - Number of rows in the grid
173    /// * `cols` - Number of columns in the grid
174    pub fn new(activity: &mut Activity, rows: i32, cols: i32, parent: Option<i64>) -> Result<Self> {
175        let mut params = json!({
176            "aid": activity.id(),
177            "rows": rows,
178            "cols": cols
179        });
180        
181        // Only set parent if explicitly provided
182        if let Some(parent_id) = parent {
183            params["parent"] = json!(parent_id);
184        }
185        
186        let response = activity.send_read(&json!({
187            "method": "createGridLayout",
188            "params": params
189        }))?;
190        
191        let id = response
192            .as_i64()
193            .ok_or_else(|| crate::error::GuiError::InvalidResponse("Invalid id".to_string()))?;
194        
195        Ok(GridLayout {
196            view: View::new(id),
197            aid: activity.id(),
198            rows,
199            cols,
200        })
201    }
202    
203    /// Get the view ID
204    pub fn id(&self) -> i64 {
205        self.view.id()
206    }
207    
208    /// Get the underlying View
209    pub fn view(&self) -> &View {
210        &self.view
211    }
212}
213
214/// A HorizontalScrollView provides horizontal scrolling for content
215///
216/// ## Important Usage Notes for TabLayout
217///
218/// When using HorizontalScrollView with TabLayout for page switching:
219///
220/// 1. **Use pixel units**: Page widths must be set using `set_width_px()`, not `set_width()`
221/// 2. **Match dimensions**: Use `get_dimensions()` to get screen width, then set each page width to match
222/// 3. **Scroll position**: Calculate scroll position as `page_width * tab_index`
223///
224/// ### Example
225///
226/// ```rust,no_run
227/// # use termux_gui::{Activity, Result};
228/// # fn example(activity: &mut Activity, root_id: i64, content_id: i64) -> Result<()> {
229/// // Create HorizontalScrollView with snapping and no scrollbar
230/// let scroll = activity.create_horizontal_scroll_view_with_params(
231///     Some(root_id), true, true, true
232/// )?;
233///
234/// // Get width in pixels
235/// let (page_width, _) = scroll.view().get_dimensions(activity)?;
236///
237/// // Create pages with pixel-based width
238/// let page1 = activity.create_linear_layout(Some(content_id))?;
239/// page1.view().set_width_px(activity, page_width)?;  // Use px, not dp!
240///
241/// // Scroll to page 2
242/// scroll.set_scroll_position(activity, page_width * 2, 0, true)?;
243/// # Ok(())
244/// # }
245/// ```
246pub struct HorizontalScrollView {
247    view: View,
248    #[allow(dead_code)]
249    aid: i64,
250}
251
252impl HorizontalScrollView {
253    /// Create a new HorizontalScrollView
254    pub fn new(activity: &mut Activity, parent: Option<i64>) -> Result<Self> {
255        let mut params = json!({
256            "aid": activity.id(),
257            "nobar": false,
258            "snapping": false,
259            "fillviewport": true  // Set to true to allow child views to fill the viewport
260        });
261        
262        // Only set parent if explicitly provided
263        if let Some(parent_id) = parent {
264            params["parent"] = json!(parent_id);
265        }
266        
267        let response = activity.send_read(&json!({
268            "method": "createHorizontalScrollView",
269            "params": params
270        }))?;
271        
272        let id = response
273            .as_i64()
274            .ok_or_else(|| crate::error::GuiError::InvalidResponse("Invalid id".to_string()))?;
275        
276        Ok(HorizontalScrollView {
277            view: View::new(id),
278            aid: activity.id(),
279        })
280    }
281    
282    /// Create a new HorizontalScrollView with custom parameters
283    pub fn new_with_params(activity: &mut Activity, parent: Option<i64>, 
284                          fillviewport: bool, snapping: bool, nobar: bool) -> Result<Self> {
285        let mut params = json!({
286            "aid": activity.id(),
287            "nobar": nobar,
288            "snapping": snapping,
289            "fillviewport": fillviewport
290        });
291        
292        if let Some(parent_id) = parent {
293            params["parent"] = json!(parent_id);
294        }
295        
296        let response = activity.send_read(&json!({
297            "method": "createHorizontalScrollView",
298            "params": params
299        }))?;
300        
301        let id = response
302            .as_i64()
303            .ok_or_else(|| crate::error::GuiError::InvalidResponse("Invalid id".to_string()))?;
304        
305        Ok(HorizontalScrollView {
306            view: View::new(id),
307            aid: activity.id(),
308        })
309    }
310    
311    /// Get the view ID
312    pub fn id(&self) -> i64 {
313        self.view.id()
314    }
315    
316    /// Get the underlying View
317    pub fn view(&self) -> &View {
318        &self.view
319    }
320    
321    /// Get the scroll position (x, y) in pixels
322    pub fn get_scroll_position(&self, activity: &mut Activity) -> Result<(i32, i32)> {
323        let response = activity.send_read(&json!({
324            "method": "getScrollPosition",
325            "params": {
326                "aid": self.aid,
327                "id": self.view.id()
328            }
329        }))?;
330        
331        // Response is an array [x, y]
332        if let Some(arr) = response.as_array() {
333            let x = arr.get(0).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
334            let y = arr.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
335            Ok((x, y))
336        } else {
337            Ok((0, 0))
338        }
339    }
340    
341    /// Set the scroll position
342    /// 
343    /// # Arguments
344    /// * `x` - Horizontal scroll position in pixels
345    /// * `y` - Vertical scroll position in pixels (usually 0 for HorizontalScrollView)
346    /// * `smooth` - Whether to scroll smoothly or jump immediately
347    pub fn set_scroll_position(&self, activity: &mut Activity, x: i32, y: i32, smooth: bool) -> Result<()> {
348        activity.send(&json!({
349            "method": "setScrollPosition",
350            "params": {
351                "aid": self.aid,
352                "id": self.view.id(),
353                "x": x,
354                "y": y,
355                "soft": smooth
356            }
357        }))?;
358        Ok(())
359    }
360}
361
362/// A SwipeRefreshLayout provides pull-to-refresh functionality
363pub struct SwipeRefreshLayout {
364    view: View,
365    aid: i64,
366}
367
368impl SwipeRefreshLayout {
369    /// Create a new SwipeRefreshLayout
370    pub fn new(activity: &mut Activity, parent: Option<i64>) -> Result<Self> {
371        let mut params = json!({
372            "aid": activity.id()
373        });
374        
375        // Only set parent if explicitly provided
376        if let Some(parent_id) = parent {
377            params["parent"] = json!(parent_id);
378        }
379        
380        let response = activity.send_read(&json!({
381            "method": "createSwipeRefreshLayout",
382            "params": params
383        }))?;
384        
385        let id = response
386            .as_i64()
387            .ok_or_else(|| crate::error::GuiError::InvalidResponse("Invalid id".to_string()))?;
388        
389        Ok(SwipeRefreshLayout {
390            view: View::new(id),
391            aid: activity.id(),
392        })
393    }
394    
395    /// Get the view ID
396    pub fn id(&self) -> i64 {
397        self.view.id()
398    }
399    
400    /// Get the underlying View
401    pub fn view(&self) -> &View {
402        &self.view
403    }
404    
405    /// Set whether the refresh animation is showing
406    /// 
407    /// Call with false after refresh is complete to stop the animation
408    pub fn set_refreshing(&self, activity: &mut Activity, refreshing: bool) -> Result<()> {
409        activity.send(&json!({
410            "method": "setRefreshing",
411            "params": {
412                "aid": self.aid,
413                "id": self.view.id(),
414                "refresh": refreshing
415            }
416        }))?;
417        Ok(())
418    }
419}
420
421/// A TabLayout displays a horizontal row of tabs
422/// 
423/// TabLayout is useful for creating tabbed interfaces. It emits 'itemselected' 
424/// events when a tab is clicked, with the tab index as the value.
425pub struct TabLayout {
426    view: View,
427    aid: i64,
428}
429
430impl TabLayout {
431    /// Create a new TabLayout
432    pub fn new(activity: &mut Activity, parent: Option<i64>) -> Result<Self> {
433        let mut params = json!({
434            "aid": activity.id()
435        });
436        
437        // Only set parent if explicitly provided
438        if let Some(parent_id) = parent {
439            params["parent"] = json!(parent_id);
440        }
441        
442        let response = activity.send_read(&json!({
443            "method": "createTabLayout",
444            "params": params
445        }))?;
446        
447        let id = response
448            .as_i64()
449            .ok_or_else(|| crate::error::GuiError::InvalidResponse("Invalid id".to_string()))?;
450        
451        Ok(TabLayout {
452            view: View::new(id),
453            aid: activity.id(),
454        })
455    }
456    
457    /// Get the view ID
458    pub fn id(&self) -> i64 {
459        self.view.id()
460    }
461    
462    /// Get the underlying View
463    pub fn view(&self) -> &View {
464        &self.view
465    }
466    
467    /// Set the list of tab labels
468    /// 
469    /// # Arguments
470    /// * `tabs` - A slice of strings representing the tab labels
471    /// 
472    /// # Example
473    /// ```no_run
474    /// tab_layout.set_list(activity, &["Page 1", "Page 2", "Page 3"])?;
475    /// ```
476    pub fn set_list(&self, activity: &mut Activity, tabs: &[&str]) -> Result<()> {
477        activity.send(&json!({
478            "method": "setList",
479            "params": {
480                "aid": self.aid,
481                "id": self.view.id(),
482                "list": tabs
483            }
484        }))?;
485        Ok(())
486    }
487    
488    /// Programmatically select a tab
489    /// 
490    /// # Arguments
491    /// * `index` - The zero-based index of the tab to select
492    /// 
493    /// # Example
494    /// ```no_run
495    /// // Select the second tab (index 1)
496    /// tab_layout.select_tab(activity, 1)?;
497    /// ```
498    pub fn select_tab(&self, activity: &mut Activity, index: usize) -> Result<()> {
499        activity.send(&json!({
500            "method": "selectTab",
501            "params": {
502                "aid": self.aid,
503                "id": self.view.id(),
504                "tab": index
505            }
506        }))?;
507        Ok(())
508    }
509}