Skip to main content

waterui_cli/gtk4/
backend.rs

1//! GTK4 backend configuration and initialization.
2
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    backend::Backend,
9    build::BuildOptions,
10    device::Artifact,
11    gtk4::platform::{build_gtk4, clean_gtk4, is_gtk4_platform, package_gtk4},
12    platform::{PackageOptions, TargetBackend, TargetPlatform},
13    project::Project,
14    templates::{self, TemplateContext},
15};
16
17/// Configuration for the GTK4 backend in a `WaterUI` project.
18///
19/// `[backend.gtk4]` in `Water.toml`
20#[derive(Debug, Serialize, Deserialize, Clone)]
21pub struct Gtk4Backend {
22    #[serde(
23        default = "default_gtk4_project_path",
24        skip_serializing_if = "is_default_gtk4_project_path"
25    )]
26    project_path: PathBuf,
27}
28
29impl Gtk4Backend {
30    /// Create a new GTK4 backend configuration with default settings.
31    #[must_use]
32    pub fn new() -> Self {
33        Self {
34            project_path: default_gtk4_project_path(),
35        }
36    }
37
38    /// Set a custom project path (defaults to "gtk4").
39    #[must_use]
40    pub fn with_project_path(mut self, path: impl Into<PathBuf>) -> Self {
41        self.project_path = path.into();
42        self
43    }
44
45    /// Get the path to the GTK4 project within the `WaterUI` project.
46    #[must_use]
47    pub const fn project_path(&self) -> &PathBuf {
48        &self.project_path
49    }
50
51    /// Check whether managed GTK backend files differ from the current templates.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error when the application dependency graph or templates
56    /// cannot be resolved.
57    pub async fn requires_regeneration(project: &Project) -> eyre::Result<bool> {
58        let backend_dir = project.backend_path::<Self>();
59        let ctx = Self::template_context(project).await?;
60        for (relative, expected) in
61            templates::gtk4::rendered_outputs(&ctx, &project.gtk_backend_crate_name())?
62        {
63            match std::fs::read(backend_dir.join(relative)) {
64                Ok(existing) if existing == expected => {}
65                Ok(_) | Err(_) => return Ok(true),
66            }
67        }
68        Ok(false)
69    }
70
71    async fn template_context(project: &Project) -> eyre::Result<TemplateContext> {
72        let manifest = project.manifest();
73        let app_name = manifest
74            .package
75            .name
76            .chars()
77            .filter(|c| c.is_alphanumeric())
78            .collect::<String>();
79        Ok(TemplateContext::for_project_manifest(
80            manifest,
81            project.crate_name().clone(),
82            app_name,
83            &project.resolved_framework().await?,
84        )
85        .with_backend_project_path(project.backend_path::<Self>())
86        .with_project_root_path(project.root().to_path_buf())
87        .with_webview_enabled(project.uses_standard_webview().await?)
88        .with_chromium_enabled(project.links_runtime_package("waterui-chromium").await?)
89        .with_browser_engine(project.linked_browser_engine().await?))
90    }
91}
92
93impl Default for Gtk4Backend {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl Backend for Gtk4Backend {
100    const DEFAULT_PATH: &'static str = "gtk4";
101
102    // GTK4 uses cargo's target directory for build caches
103    // Since GTK4 project is a simple Rust binary crate, it uses the workspace target
104    // No need to preserve local target - it's part of the workspace
105    const CACHE_PATHS: &'static [&'static str] = &[];
106
107    fn path(&self) -> &Path {
108        &self.project_path
109    }
110
111    async fn init(project: &Project) -> Result<Self, crate::backend::FailToInitBackend> {
112        let project_path = default_gtk4_project_path();
113        let ctx = Self::template_context(project)
114            .await
115            .map_err(crate::backend::FailToInitBackend::Config)?;
116
117        templates::gtk4::scaffold(
118            &project.backend_path::<Self>(),
119            &ctx,
120            &project.gtk_backend_crate_name(),
121        )
122        .await
123        .map_err(crate::backend::FailToInitBackend::Io)?;
124
125        Ok(Self { project_path })
126    }
127
128    fn supports(&self, platform: TargetPlatform) -> bool {
129        is_gtk4_platform(platform)
130    }
131
132    async fn build(
133        &self,
134        project: &Project,
135        _platform: TargetPlatform,
136        options: BuildOptions,
137    ) -> eyre::Result<PathBuf> {
138        project
139            .browser_runtime_plan(TargetPlatform::Linux, TargetBackend::Gtk4)
140            .await?;
141        build_gtk4(project, options).await
142    }
143
144    async fn package(
145        &self,
146        project: &Project,
147        _platform: TargetPlatform,
148        options: PackageOptions,
149    ) -> eyre::Result<Artifact> {
150        package_gtk4(project, options).await
151    }
152
153    async fn clean(&self, project: &Project, _platform: TargetPlatform) -> eyre::Result<()> {
154        clean_gtk4(project).await
155    }
156}
157
158fn default_gtk4_project_path() -> PathBuf {
159    PathBuf::from("gtk4")
160}
161
162fn is_default_gtk4_project_path(s: &Path) -> bool {
163    s == Path::new("gtk4")
164}