28 Commits

Author SHA1 Message Date
c1f920a632 Update .gitlab-ci.yml file 2025-07-20 21:26:37 +00:00
de0a8dc342 Merge branch 'master' of gitlab.dukantic.fr:dukantic/polymusic 2025-07-20 23:15:49 +02:00
9a790af3d5 add optimisation 2025-07-20 22:44:43 +02:00
bc64f1b81d Update .gitlab-ci.yml file 2025-07-20 20:38:59 +00:00
2db3bfbc6d Update .gitlab-ci.yml file 2025-07-20 20:29:16 +00:00
4880c81d80 Update .gitlab-ci.yml file 2025-07-20 20:13:45 +00:00
b4210ce972 Update .gitlab-ci.yml file 2025-07-20 20:10:34 +00:00
b6f0a5025a Update .gitlab-ci.yml file 2025-07-20 19:54:07 +00:00
4c02ea41da Update .gitlab-ci.yml file 2025-07-20 19:45:19 +00:00
a1ba9e8dde Update .gitlab-ci.yml file 2025-07-20 19:39:33 +00:00
58624eb2ff Update .gitlab-ci.yml file 2025-07-20 19:35:39 +00:00
ad47e91d0a name and version 2025-07-20 21:29:06 +02:00
aa5283a000 name and version 2025-07-20 21:28:31 +02:00
607f65c913 cargo fix 2025-07-19 18:56:22 +02:00
82ca2ced24 historic , undo and redo 2025-07-19 18:56:06 +02:00
bb64de1955 remove to do list and add information 2025-07-19 17:08:35 +02:00
54949a9db0 remove file not used 2025-07-19 17:03:14 +02:00
d841c9fdb4 zoom on time line 2025-07-19 13:34:15 +02:00
ad6d09b8a5 cargo fix 2025-07-19 01:18:50 +02:00
e7b1000205 load menu and save carfully 2025-07-19 01:18:34 +02:00
625828f7ed save 2025-07-18 20:33:00 +02:00
5cd8bf90c7 fix cargo 2025-07-18 20:18:22 +02:00
fc54c4b9ee menu bar 2025-07-18 19:51:28 +02:00
46861440fd fix by cargo 2025-07-18 18:22:19 +02:00
fdfea98c50 shortcut 2025-07-18 18:21:36 +02:00
a512eff61d fix 2025-07-18 17:51:36 +02:00
bbeff369e8 fix 2025-07-18 17:51:17 +02:00
3788c3e631 fix 2025-07-17 22:18:44 +02:00
10 changed files with 726 additions and 247 deletions

38
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,38 @@
stages:
- build
build_linux:
stage: build
script:
- cargo build --release
- cp target/release/polymusic .
artifacts:
paths:
- polymusic
- fonts/
- assets/
expire_in: 1 month
tags:
- linux
rules:
- if: $CI_COMMIT_TAG
when: on_success
- when: never
build_windows:
stage: build
script:
- cargo build --release --target x86_64-pc-windows-gnu
- cp target/x86_64-pc-windows-gnu/release/polymusic.exe .
artifacts:
paths:
- polymusic.exe
- fonts/
- assets/
expire_in: 1 month
tags:
- windows
rules:
- if: $CI_COMMIT_TAG
when: on_success
- when: never

28
Cargo.lock generated
View File

@@ -431,6 +431,15 @@ dependencies = [
"piper",
]
[[package]]
name = "borsh"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
dependencies = [
"cfg_aliases 0.2.1",
]
[[package]]
name = "bumpalo"
version = "3.17.0"
@@ -1638,7 +1647,7 @@ dependencies = [
"once_cell",
"palette",
"rustc-hash 2.1.1",
"smol_str",
"smol_str 0.2.2",
"thiserror",
"web-time",
]
@@ -2824,8 +2833,8 @@ dependencies = [
]
[[package]]
name = "polygomusic"
version = "0.1.0"
name = "polymusic"
version = "1.2.0"
dependencies = [
"iced",
"iced_aw",
@@ -2834,6 +2843,7 @@ dependencies = [
"regex",
"serde",
"serde_json",
"smol_str 0.3.2",
"tokio",
]
@@ -3352,6 +3362,16 @@ dependencies = [
"serde",
]
[[package]]
name = "smol_str"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d"
dependencies = [
"borsh",
"serde",
]
[[package]]
name = "softbuffer"
version = "0.4.6"
@@ -4647,7 +4667,7 @@ dependencies = [
"rustix 0.38.44",
"sctk-adwaita",
"smithay-client-toolkit",
"smol_str",
"smol_str 0.2.2",
"tracing",
"unicode-segmentation",
"wasm-bindgen",

View File

@@ -1,6 +1,6 @@
[package]
name = "polygomusic"
version = "0.1.0"
name = "polymusic"
version = "1.2.0"
edition = "2024"
build = "build.rs"
@@ -13,3 +13,9 @@ serde_json = "1.0.140"
regex = "1.11.1"
iced_aw = {version = "0.12.2", default-features = true}
iced_fonts = "0.2.1"
smol_str = "0.3.2"
[profile.release]
opt-level = 3 # optimisation maximale (0 à 3)
lto = true # Link Time Optimization, optimise le binaire final
codegen-units = 1 # pour meilleure optimisation (par défaut c'est plus pour vitesse de compilation)
panic = 'abort' # pour binaire plus petit et rapide, abandonne unwind

View File

@@ -6,6 +6,7 @@ Polymusic is an innovative music application inspired
and rich audio textures, creating a unique auditory experience.
## Instalation
### With source code
>Install Rust and Cargo
>https://www.rust-lang.org/tools/install
@@ -13,6 +14,9 @@ Polymusic is an innovative music application inspired
```
cargo run
```
### Release
Release is available on gitlab. Download windows ou linux version.
Execute `Polymusic` or `Polymusic.exe`
## Files
@@ -24,10 +28,3 @@ The `assets` folder contains all the sounds you can play. You can add more.
- FLAC
- WAV
## To Do List
- [ ] Custom Theme
- [ ] RGB
- [ ] Edit angle by degree
- [ ] Sound on folder
- [ ] Global sound

257
src/gui.rs Normal file
View File

@@ -0,0 +1,257 @@
use crate::message::Message;
use iced::alignment::{Horizontal, Vertical};
use iced::widget::{TextInput, column, text};
use iced::{Element, Theme};
use iced::{
Length, Padding,
widget::{Column, Container, button, canvas, container, pick_list, row, scrollable, slider},
};
use iced_aw::menu::{self, Item};
use iced_aw::menu_bar;
use iced_aw::widget::color_picker;
use iced_aw::widget::menu::Menu;
use std::f32::consts::PI;
use crate::MyApp;
pub fn music_view(app: &MyApp) -> iced::Element<Message> {
let mut i = 0;
let entries = app.all_sounds.clone();
//Create all polygon options
let polygon_rows: Vec<Element<Message>> = app
.music
.current_frame(app.current_delta)
.polygons
.iter()
.map(|polygon| {
let current_index = i;
let but = button(text("").size(20).center()).on_press(Message::SubmitColor(i));
let c = column![
row![
text(&polygon.name).size(24),
button(text("").size(20)).on_press(Message::Remove(i)),
color_picker(
polygon.show_color_picker,
polygon.color,
but,
Message::CancelColor(i),
move |color| Message::ChooseColor(i, color)
),
pick_list(entries.clone(), Some(&polygon.sound_name), move |s| {
Message::ChangeSound(current_index, s)
})
.text_size(20),
]
.spacing(20),
row![
TextInput::new("90", &polygon.global_teta.to_degrees().floor().to_string())
.on_input(move |new_value| Message::ChangeDegree(current_index, new_value))
.width(Length::FillPortion(1)),
row![
slider(0.0..=2.0 * PI, polygon.global_teta, move |f| {
Message::ChangeTeta(current_index, f)
})
.step(2. * PI / 10_000.)
.width(Length::FillPortion(9))
]
.padding(Padding::from(16)),
]
.spacing(5),
]
.spacing(10)
.into();
i += 1;
c
})
.collect();
let ngon_options: Vec<String> = (5..=42).map(|sides| format!("Ngon{sides}")).collect();
let all_options: Vec<String> = [
"Segment",
"Triangle",
"Square",
"Nr6In30",
"Nr7In30",
"Nr8In30",
"Nr9In30",
"Nr8In42",
"Nr9In42",
"Nr10aIn42",
"Nr10bIn42",
]
.iter()
.map(|s| s.to_string())
.chain(ngon_options)
.collect();
let polygon_column = scrollable(Column::with_children(polygon_rows).spacing(24));
let menu_tpl_1 = |items| Menu::new(items).max_width(200.0).offset(15.0).spacing(5.0);
let save_menu = menu_tpl_1(vec![
Item::new(button("Save File").on_press(Message::Save)),
Item::new(button("Load Menu").on_press(Message::GoToLoadView).style(
|theme: &Theme, status| {
let palette = theme.extended_palette();
match status {
button::Status::Active => {
if !app.already_save {
button::Style::default().with_background(palette.danger.strong.color)
} else {
button::primary(theme, status)
}
}
_ => button::primary(theme, status),
}
},
)),
Item::new("CTRL+F for quit without save."),
]);
column![
menu_bar!((text("File").size(20), save_menu)(
text("Shortcuts").size(20),
menu_tpl_1(vec![
Item::new("󰐊 SPACE"),
Item::new("󰆓 CTRL+S"),
Item::new("󰈼 ARROW UP"),
Item::new("󰕌 CTRL+Z"),
Item::new("󰑎 CTRL+Y"),
])
))
.draw_path(menu::DrawPath::Backdrop)
.spacing(20),
text(&app.music.file_name)
.width(Length::FillPortion(1))
.size(32),
row![
container(
canvas(app.music.current_frame(app.current_delta))
.height(Length::FillPortion(1))
.width(Length::FillPortion(1))
),
column![
text("Polygon options").size(26),
pick_list(all_options, Some("Choose polygon".to_string()), |s| {
Message::AddPolygon(s)
})
.text_size(18),
polygon_column,
]
.spacing(10)
.height(Length::FillPortion(1))
.width(Length::FillPortion(2)),
]
.height(Length::FillPortion(2))
.spacing(20),
column![
row![
button(text(if app.paused { "󰐊" } else { "󰏤" }).size(28).center())
.on_press(if app.can_unpaused {
Message::TogglePaused
} else {
Message::None
})
.width(Length::FillPortion(1)),
row![
TextInput::new("MM:SS:CS", &app.str_time)
.on_input(|new_value| Message::ChangeDeltaString(new_value))
.size(28),
text("/").size(30),
TextInput::new("MM:SS:CS", &app.str_music_length)
.on_input(|new_value| Message::LengthChange(new_value))
.size(28),
]
.width(Length::FillPortion(10)),
TextInput::new("1.0", &format!("{:.1} sec/rev", &app.music.nb_sec_for_rev))
.on_input(|new_value| Message::ChangeNbPerSec(new_value))
.size(28)
.width(Length::FillPortion(2)),
button(text("").size(28).center())
.on_press(Message::SlidePointLeft)
.width(Length::FillPortion(1)),
button(text("").size(28).center())
.on_press(Message::AddPoint)
.width(Length::FillPortion(1)),
button(text("").size(28).center())
.on_press(Message::SlidePointRight)
.width(Length::FillPortion(1)),
button(text("").size(28).center())
.on_press(Message::RemovePoint)
.width(Length::FillPortion(1)),
]
.spacing(20),
column![
/*
slider(0.0..=app.music.length, self.current_delta, move |f| {
Message::ChangeDelta(f)
})
.step(&app.music.length / 10_000.),*/
canvas(&app.music)
.height(Length::FillPortion(1))
.width(Length::FillPortion(1))
]
.spacing(0),
]
.spacing(20)
.height(Length::FillPortion(1))
.width(Length::FillPortion(1)),
]
.spacing(25)
.padding(25)
.into()
}
pub fn load_file_view(app: &MyApp) -> iced::Element<Message> {
Container::new(
column![
text("Polymusic").size(42),
row![
column![
TextInput::new("Name File", &app.file_name_to_creat)
.on_input(|new_value| Message::SetFileNameCreat(new_value)),
button("Create File").on_press(Message::CreatFile),
text(if app.show_warning_message_creat {
"Warning: File already exists. Delete it or change the file name!"
} else {
""
})
.style(|theme: &Theme| {
let palette = theme.extended_palette();
text::Style {
color: Some(palette.danger.strong.color),
}
},)
]
.spacing(10)
.width(200)
.height(200)
.align_x(Horizontal::Center),
column![
pick_list(
app.all_saves.clone(),
Some(&app.music.file_name),
move |s| Message::FileNameChanged(s),
),
button("Load File").on_press(Message::Load),
text("")
]
.spacing(10)
.width(200)
.height(200)
.align_x(Horizontal::Center)
]
.spacing(32)
.align_y(Vertical::Center),
]
.spacing(64)
.align_x(Horizontal::Center),
)
.width(Length::Fill)
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill)
.into()
}

53
src/history.rs Normal file
View File

@@ -0,0 +1,53 @@
use crate::message::Message;
#[derive(Clone, Debug)]
struct HistoricItem {
old: Message,
new: Message,
delta: f32,
}
#[derive(Debug)]
pub struct Historic {
past: Vec<HistoricItem>,
future: Vec<HistoricItem>,
pub ignore_add: bool,
}
impl Historic {
pub fn new() -> Self {
Historic {
past: vec![],
future: vec![],
ignore_add: false,
}
}
pub fn add(&mut self, old: Message, new: Message, delta: f32) {
if !self.ignore_add {
self.future = vec![];
self.past.push(HistoricItem {
old: old,
new: new,
delta: delta,
});
}
}
pub fn undo(&mut self) -> Option<(Message, f32)> {
if let Some(item) = self.past.pop() {
self.future.push(item.clone());
Some((item.old.clone(), item.delta))
} else {
None
}
}
pub fn redo(&mut self) -> Option<(Message, f32)> {
if let Some(item) = self.future.pop() {
self.past.push(item.clone());
Some((item.new.clone(), item.delta))
} else {
None
}
}
}

View File

@@ -1,26 +1,32 @@
mod polygon_draw;
use iced::keyboard::Modifiers;
use polygon_draw::Polygon;
mod music;
use music::Music;
mod history;
use history::Historic;
mod message;
use message::Message;
mod utils;
use utils::{is_delta_format_valid, str_to_sec};
use utils::{delta_to_string, is_delta_format_valid, str_to_sec};
use std::fs;
use iced::widget::{TextInput, column, text};
use iced::{
Color, Length, Padding, Task, Theme, padding,
widget::{Column, button, canvas, container, pick_list, row, scrollable, slider},
};
use iced::{Element, Font, Subscription};
use iced_aw::widget::color_picker;
mod gui;
use gui::{load_file_view, music_view};
use iced::Font;
use iced::Theme;
use iced::{
Color, Event, Task,
event::{self, Status},
keyboard::{Key, key::Named},
};
use std::f32::consts::PI;
use std::time::Instant;
use kira::{
@@ -29,7 +35,6 @@ use kira::{
const FONT_BYTES: &[u8] = include_bytes!("../fonts/EnvyCodeRNerdFontMono-Regular.ttf");
const FONT: Font = Font::with_name("EnvyCodeR Nerd Font Mono");
use crate::utils::delta_to_string;
fn main() -> iced::Result {
let polytheme: Theme = {
let back = Color::from_rgb8(39, 63, 79);
@@ -62,7 +67,6 @@ fn main() -> iced::Result {
struct MyApp {
music: Music,
time_last_frame: Instant,
show_save_panel: bool,
paused: bool,
audio_manager: AudioManager,
all_sounds: Vec<String>,
@@ -71,6 +75,11 @@ struct MyApp {
str_music_length: String,
str_time: String,
can_unpaused: bool,
already_save: bool,
mode_file_load: bool,
file_name_to_creat: String,
show_warning_message_creat: bool,
historic: Historic,
}
impl MyApp {
@@ -78,13 +87,10 @@ impl MyApp {
let manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())
.expect("Error to load AudioManager");
//let font_bytes = include_bytes!("../fonts/");
(
Self {
time_last_frame: Instant::now(),
audio_manager: manager,
show_save_panel: false,
paused: true,
all_sounds: load_path_sounds(),
all_saves: load_path_saves(),
@@ -93,6 +99,11 @@ impl MyApp {
str_music_length: "01:00:00".to_string(),
str_time: "00:00:00".to_string(),
can_unpaused: true,
already_save: true,
mode_file_load: true,
file_name_to_creat: "Default File Name".to_string(),
show_warning_message_creat: false,
historic: Historic::new(),
},
Task::none(),
)
@@ -100,7 +111,13 @@ impl MyApp {
fn update(&mut self, message: Message) {
match message {
Message::AddPolygon(s) => {
self.music.add_polygon(self.current_delta, s);
self.music.add_polygon(self.current_delta, s.clone());
self.already_save = false;
self.historic.add(
Message::Remove(usize::MAX),
Message::AddPolygon(s),
self.current_delta,
);
}
Message::Tick => {
if self.current_delta >= self.music.length {
@@ -117,21 +134,42 @@ impl MyApp {
}
}
Message::Remove(i) => {
self.music.remove_polygon(self.current_delta, i);
let name = self.music.remove_polygon(self.current_delta, i);
self.already_save = false;
self.historic.add(
Message::AddPolygon(name),
Message::Remove(i),
self.current_delta,
);
}
Message::ChangeTeta(i, teta) => {
self.music.set_teta(self.current_delta, i, teta);
let old_teta = self.music.set_teta(self.current_delta, i, teta);
self.already_save = false;
self.historic.add(
Message::ChangeTeta(i, old_teta),
Message::ChangeTeta(i, teta),
self.current_delta,
);
}
Message::ChangeSound(i, s) => {
let sound = StaticSoundData::from_file(format!("./assets/{s}"))
.expect("Fail to load audio");
self.music.set_sound(self.current_delta, i, sound, s);
let old_sound = self
.music
.set_sound(self.current_delta, i, sound, s.clone());
self.already_save = false;
self.historic.add(
Message::ChangeSound(i, old_sound),
Message::ChangeSound(i, s),
self.current_delta,
);
}
Message::Save => {
let json = serde_json::to_string_pretty(&self.music).unwrap();
fs::write(format!("./saves/{0}.pmx", &self.music.file_name), json).unwrap();
self.all_saves = load_path_saves();
self.already_save = true;
}
Message::Load => {
let json = fs::read_to_string(format!("./saves/{0}.pmx", &self.music.file_name));
@@ -140,19 +178,36 @@ impl MyApp {
let decoded: Music = serde_json::from_str(&j).unwrap();
self.music = decoded;
self.music.update_frame();
self.mode_file_load = false;
}
Err(e) => {
eprintln!("Error, no saves with this name to load, {e} ");
}
}
}
Message::ToggleSavePanel => self.show_save_panel = !self.show_save_panel,
Message::SetFileNameCreat(s) => self.file_name_to_creat = s,
Message::CreatFile => {
if self.all_saves.contains(&self.file_name_to_creat) {
self.show_warning_message_creat = true;
} else {
self.mode_file_load = false;
self.music = Music::default();
self.music.file_name = self.file_name_to_creat.clone();
self.update(Message::Save);
}
}
Message::FileNameChanged(s) => self.music.file_name = s,
Message::LengthChange(s) => {
if is_delta_format_valid(&s) {
let sec = str_to_sec(&s);
if sec > 0. {
self.historic.add(
Message::LengthChange(delta_to_string(self.music.length)),
Message::LengthChange(s.clone()),
self.current_delta,
);
self.music.length = sec;
self.already_save = false;
}
}
self.str_music_length = s;
@@ -172,28 +227,54 @@ impl MyApp {
}
Message::ChangeDeltaString(s) => {
if is_delta_format_valid(&s) {
self.already_save = false;
self.update(Message::ChangeDelta(str_to_sec(&s)));
}
self.str_time = s;
}
Message::ReAddPoint => {
self.already_save = false;
self.music.add_point_old();
}
Message::AddPoint => {
self.already_save = false;
self.music.add_point(self.current_delta);
self.historic
.add(Message::RemovePoint, Message::AddPoint, self.current_delta);
}
Message::RemovePoint => {
self.already_save = false;
self.music.remove_point(self.current_delta);
self.historic.add(
Message::ReAddPoint,
Message::RemovePoint,
self.current_delta,
);
}
Message::ClickedOnTimeLine(f) => {
self.update(Message::ChangeDelta(f));
}
Message::SlidePointLeft => {
self.already_save = false;
self.music.slide_to_left(self.current_delta);
self.music.fix_teta(self.current_delta);
self.update_canvas_if_paused();
self.historic.add(
Message::SlidePointRight,
Message::SlidePointLeft,
self.current_delta,
);
}
Message::SlidePointRight => {
self.already_save = false;
self.music.slide_to_right(self.current_delta);
self.music.fix_teta(self.current_delta);
self.update_canvas_if_paused();
self.historic.add(
Message::SlidePointLeft,
Message::SlidePointRight,
self.current_delta,
);
}
Message::ChangeDegree(i, s) => {
let mut mut_s = s;
@@ -203,20 +284,30 @@ impl MyApp {
match mut_s.parse::<f32>() {
Ok(val) => {
if val >= 0. && val <= 360. {
self.update(Message::ChangeTeta(i, (val).to_radians()))
self.update(Message::ChangeTeta(i, (val).to_radians()));
self.already_save = false;
}
}
Err(_) => {}
}
}
Message::ChangeNbPerSec(s) => {
let mut_s = s.trim_end_matches(" rev/sec");
let mut_s = s.trim_end_matches(" sec/rev");
if mut_s.len() != s.len() {
match mut_s.parse::<f32>() {
Ok(val) => {
let val = (val * 10.).floor() / 10.;
if val >= 1. && val < 1000. {
self.music.nb_sec_for_rev = val
self.historic.add(
Message::ChangeNbPerSec(format!(
"{:.1} sec/rev",
self.music.nb_sec_for_rev
)),
Message::ChangeNbPerSec(s.clone()),
self.current_delta,
);
self.music.nb_sec_for_rev = val;
self.already_save = false;
}
}
Err(_) => {}
@@ -235,197 +326,114 @@ impl MyApp {
self.music.set_color_picker(self.current_delta, i, true);
}
Message::ChooseColor(i, color) => {
self.music.set_color(self.current_delta, i, color);
let old_color = self.music.set_color(self.current_delta, i, color);
self.music.set_color_picker(self.current_delta, i, false);
self.can_unpaused = true;
self.already_save = false;
self.historic.add(
Message::ChooseColor(i, old_color),
Message::ChooseColor(i, color),
self.current_delta,
);
}
Message::GoToLoadView => {
if self.already_save {
self.historic = Historic::new();
self.paused = true;
self.file_name_to_creat = "Default File Name".to_string();
self.show_warning_message_creat = false;
self.mode_file_load = true
}
}
Message::ForceToQuit => {
self.already_save = true;
}
Message::Undo => {
if let Some(item) = self.historic.undo() {
self.historic.ignore_add = true;
self.update(Message::ChangeDelta(item.1));
self.update(item.0);
self.historic.ignore_add = false;
}
}
Message::Redo => {
if let Some(item) = self.historic.redo() {
self.historic.ignore_add = true;
self.update(Message::ChangeDelta(item.1));
self.update(item.0);
self.historic.ignore_add = false;
}
}
Message::None => {}
}
}
fn view(&self) -> iced::Element<Message> {
let mut i = 0;
let entries = self.all_sounds.clone();
//Create all polygon options
let polygon_rows: Vec<Element<Message>> = self
.music
.current_frame(self.current_delta)
.polygons
.iter()
.map(|polygon| {
let current_index = i;
let but = button(text("").size(20).center()).on_press(Message::SubmitColor(i));
let c = column![
row![
text(&polygon.name).size(24),
button(text("").size(20)).on_press(Message::Remove(i)),
color_picker(
polygon.show_color_picker,
polygon.color,
but,
Message::CancelColor(i),
move |color| Message::ChooseColor(i, color)
),
pick_list(entries.clone(), Some(&polygon.sound_name), move |s| {
Message::ChangeSound(current_index, s)
})
.text_size(20),
]
.spacing(20),
row![
TextInput::new("90", &polygon.global_teta.to_degrees().floor().to_string())
.on_input(move |new_value| Message::ChangeDegree(
current_index,
new_value
))
.width(Length::FillPortion(1)),
row![
slider(0.0..=2.0 * PI, polygon.global_teta, move |f| {
Message::ChangeTeta(current_index, f)
})
.step(2. * PI / 10_000.)
.width(Length::FillPortion(9))
]
.padding(Padding::from(16)),
]
.spacing(5),
]
.spacing(10)
.into();
i += 1;
c
})
.collect();
let ngon_options: Vec<String> = (5..=42).map(|sides| format!("Ngon{sides}")).collect();
let all_options: Vec<String> = [
"Segment",
"Triangle",
"Square",
"Nr6In30",
"Nr7In30",
"Nr8In30",
"Nr9In30",
"Nr8In42",
"Nr9In42",
"Nr10aIn42",
"Nr10bIn42",
]
.iter()
.map(|s| s.to_string())
.chain(ngon_options)
.collect();
let polygon_column = scrollable(Column::with_children(polygon_rows).spacing(24));
let mut save_panel: Vec<Element<Message>> = vec![
button("Toggle Save Panel")
.on_press(Message::ToggleSavePanel)
.into(),
];
if self.show_save_panel {
save_panel.push(
TextInput::new("Name File", &self.music.file_name)
.on_input(|new_value| Message::FileNameChanged(new_value))
.into(),
);
save_panel.push(button("Save").on_press(Message::Save).into());
save_panel.push(
pick_list(
self.all_saves.clone(),
Some(&self.music.file_name),
move |s| Message::FileNameChanged(s),
)
.into(),
);
save_panel.push(button("Load").on_press(Message::Load).into());
}
column![
text("Polymusic").size(32.0),
row(save_panel).spacing(20),
row![
container(
canvas(self.music.current_frame(self.current_delta))
.height(Length::FillPortion(1))
.width(Length::FillPortion(1))
),
column![
text("Polygon options").size(26),
pick_list(all_options, Some("Choose polygon".to_string()), |s| {
Message::AddPolygon(s)
})
.text_size(18),
polygon_column,
]
.spacing(10)
.height(Length::FillPortion(1))
.width(Length::FillPortion(2)),
]
.height(Length::FillPortion(2))
.spacing(20),
column![
row![
button(text(if self.paused { "󰐊" } else { "󰏤" }).size(28).center())
.on_press(if self.can_unpaused {
Message::TogglePaused
if !self.mode_file_load {
music_view(self)
} else {
Message::None
})
.width(Length::FillPortion(1)),
row![
TextInput::new("MM:SS:CS", &self.str_time)
.on_input(|new_value| Message::ChangeDeltaString(new_value))
.size(28),
text("/").size(30),
TextInput::new("MM:SS:CS", &self.str_music_length)
.on_input(|new_value| Message::LengthChange(new_value))
.size(28),
]
.width(Length::FillPortion(10)),
TextInput::new("1.0", &format!("{:.1} rev/sec", &self.music.nb_sec_for_rev))
.on_input(|new_value| Message::ChangeNbPerSec(new_value))
.size(28)
.width(Length::FillPortion(2)),
button(text("").size(28).center())
.on_press(Message::SlidePointLeft)
.width(Length::FillPortion(1)),
button(text("").size(28).center())
.on_press(Message::AddPoint)
.width(Length::FillPortion(1)),
button(text("").size(28).center())
.on_press(Message::SlidePointRight)
.width(Length::FillPortion(1)),
button(text("").size(28).center())
.on_press(Message::RemovePoint)
.width(Length::FillPortion(1)),
]
.spacing(20),
column![
/*
slider(0.0..=self.music.length, self.current_delta, move |f| {
Message::ChangeDelta(f)
})
.step(&self.music.length / 10_000.),*/
canvas(&self.music)
.height(Length::FillPortion(1))
.width(Length::FillPortion(1))
]
.spacing(0),
]
.spacing(20)
.height(Length::FillPortion(1))
.width(Length::FillPortion(1)),
]
.spacing(25)
.padding(25)
.into()
load_file_view(self)
}
}
fn subscription(&self) -> iced::Subscription<Message> {
let subscr_key = event::listen_with(|event, status, _| match (event, status) {
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Character(c),
modifiers: Modifiers::CTRL,
..
}),
Status::Ignored,
) if c.as_ref() == "z" => Some(Message::Undo),
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Character(c),
modifiers: Modifiers::CTRL,
..
}),
Status::Ignored,
) if c.as_ref() == "y" => Some(Message::Redo),
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Character(c),
modifiers: Modifiers::CTRL,
..
}),
Status::Ignored,
) if c.as_ref() == "s" => Some(Message::Save),
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Named(Named::Space),
..
}),
Status::Ignored,
) => Some(Message::TogglePaused),
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Named(Named::ArrowUp),
..
}),
Status::Ignored,
) => Some(Message::ChangeDelta(0.0)),
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Character(c),
modifiers: Modifiers::CTRL,
..
}),
Status::Ignored,
) if c.as_ref() == "f" => Some(Message::ForceToQuit),
_ => None,
});
if self.paused {
Subscription::none() // ➝ désactive toutes les subscriptions
subscr_key
} else {
iced::time::every(std::time::Duration::from_millis(16)).map(|_| Message::Tick)
iced::Subscription::batch([
subscr_key,
iced::time::every(std::time::Duration::from_millis(16)).map(|_| Message::Tick),
])
}
}
@@ -450,7 +458,7 @@ fn load_path_sounds() -> Vec<String> {
fn load_path_saves() -> Vec<String> {
fs::create_dir_all("./saves").expect("fail to creat 'saves' !");
fs::read_dir("./saves")
let mut saves: Vec<String> = fs::read_dir("./saves")
.unwrap()
.filter_map(|res| res.ok())
.map(|e| {
@@ -462,5 +470,7 @@ fn load_path_saves() -> Vec<String> {
.trim_end_matches(".pmx")
.to_string()
})
.collect()
.collect();
saves.sort();
saves
}

View File

@@ -3,28 +3,36 @@ use iced::Color;
#[derive(Debug, Clone)]
pub enum Message {
None,
ChangeNbPerSec(String),
CreatFile,
SetFileNameCreat(String),
GoToLoadView,
ForceToQuit,
Tick,
AddPolygon(String),
ChangeTeta(usize, f32),
Remove(usize),
ChangeSound(usize, String),
ToggleSavePanel,
Save,
Load,
FileNameChanged(String),
TogglePaused,
LengthChange(String),
ChangeDelta(f32),
AddPoint,
RemovePoint,
ClickedOnTimeLine(f32),
ChangeDeltaString(String),
ChangeNbPerSec(String),
AddPolygon(String),
ChangeTeta(usize, f32),
Remove(usize),
ChangeSound(usize, String),
LengthChange(String),
AddPoint,
RemovePoint,
SlidePointLeft,
SlidePointRight,
ChangeDegree(usize, String),
ReAddPoint,
ChooseColor(usize, Color),
CancelColor(usize),
SubmitColor(usize),
Undo,
Redo,
}

View File

@@ -1,15 +1,18 @@
use crate::message::Message;
use crate::utils::string_to_polygon;
use crate::polygon_draw::*;
use crate::utils::string_to_polygon;
use serde::{Deserialize, Serialize};
use kira::{AudioManager, sound::static_sound::StaticSoundData};
use iced::Vector;
use iced::event::Status;
use iced::mouse::Cursor;
use iced::widget::canvas::{Event, Geometry};
use iced::{Size, mouse};
use iced::{
Size,
mouse::{self, ScrollDelta},
};
use iced::{Vector, keyboard};
use iced::widget::canvas;
use iced::widget::canvas::Stroke;
@@ -28,6 +31,8 @@ pub struct Music {
teta: f32,
#[serde(skip)]
pub current_delta: f32,
#[serde(skip)]
point_removed: Vec<(f32, PolygonFrame)>,
}
impl Music {
@@ -70,10 +75,11 @@ impl Music {
Music {
poly_frame: vec![(0.0, PolygonFrame::default())],
nb_sec_for_rev: 1.0,
file_name: "Default Name".to_string(),
file_name: "Default File Name".to_string(),
length: 60.0,
teta: 0.,
current_delta: 0.,
point_removed: vec![],
}
}
@@ -113,19 +119,25 @@ impl Music {
index: usize,
sound: StaticSoundData,
sound_name: String,
) {
) -> String {
let current_frame = self.find_poly_frame(delta);
current_frame.polygons[index].sound = sound;
let out = current_frame.polygons[index].sound_name.clone();
current_frame.polygons[index].sound_name = sound_name;
out
}
pub fn set_teta(&mut self, delta: f32, index: usize, teta: f32) {
pub fn set_teta(&mut self, delta: f32, index: usize, teta: f32) -> f32 {
let out = self.find_poly_frame(delta).polygons[index].global_teta;
self.find_poly_frame(delta).polygons[index].global_teta = teta;
out
}
pub fn set_color(&mut self, delta: f32, index: usize, color: Color) {
pub fn set_color(&mut self, delta: f32, index: usize, color: Color) -> Color {
let current_frame = self.find_poly_frame(delta);
let out = current_frame.polygons[index].color.clone();
current_frame.polygons[index].color = color;
out
}
pub fn set_color_picker(&mut self, delta: f32, i: usize, b: bool) {
@@ -143,9 +155,19 @@ impl Music {
self.poly_frame
.insert(pos, (delta, self.current_frame(delta).clone()));
}
pub fn add_point_old(&mut self) {
if let Some(pair) = self.point_removed.pop() {
let pos = self
.poly_frame
.binary_search_by(|(d, _)| d.partial_cmp(&pair.0).unwrap())
.unwrap_or_else(|e| e);
self.poly_frame.insert(pos, pair);
}
}
pub fn remove_point(&mut self, delta: f32) {
let i = self.find_index_frame(delta);
if i != 0 {
self.point_removed.push(self.poly_frame[i].clone());
self.poly_frame.remove(i);
}
}
@@ -156,8 +178,15 @@ impl Music {
current_frame.polygons.push(poly);
}
pub fn remove_polygon(&mut self, delta: f32, i: usize) {
self.find_poly_frame(delta).polygons.remove(i);
pub fn remove_polygon(&mut self, delta: f32, i: usize) -> String {
let pf = self.find_poly_frame(delta);
let mut i = i;
if i == usize::MAX {
i = pf.polygons.len() - 1
}
let out = pf.polygons[i].name.clone();
pf.polygons.remove(i);
out
}
pub fn slide_to_left(&mut self, delta: f32) {
@@ -175,13 +204,21 @@ impl Music {
}
}
}
#[derive(Default, Debug)]
pub struct StateMusic {
mouse_left: bool,
ctrl: bool,
zoom: f32,
offset: f32,
}
impl canvas::Program<Message> for Music {
// No internal state
type State = bool;
type State = StateMusic;
fn draw(
&self,
_state: &Self::State,
state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
@@ -190,22 +227,26 @@ impl canvas::Program<Message> for Music {
let mut geo_small_frame: Vec<Geometry> = vec![];
let mut geo_cursor: Vec<Geometry> = vec![];
let frame = canvas::Frame::new(renderer, bounds.size());
let global_width = bounds.width * state.zoom;
let mut toggle_color = true;
let padding = 8.;
let w = bounds.width - (padding * 2.);
let w = global_width - (padding * 2.);
for (delta, polyframe) in &self.poly_frame {
let x = delta / self.length * w + 8.;
let x = delta / self.length * w + 8. - state.offset;
let mut back_frame = canvas::Frame::new(
renderer,
Size {
width: bounds.width,
width: global_width.clamp(0.0, bounds.width),
height: bounds.height,
},
);
back_frame.fill_rectangle(
iced::Point { x: x, y: 0.0 },
frame.size(),
Size {
width: global_width,
height: bounds.height,
},
if toggle_color {
Color::from_rgb8(27, 60, 83)
} else {
@@ -217,7 +258,7 @@ impl canvas::Program<Message> for Music {
let mut small_frame = canvas::Frame::new(
renderer,
Size {
width: bounds.width,
width: global_width.clamp(0.0, bounds.width),
height: bounds.height,
},
);
@@ -236,7 +277,7 @@ impl canvas::Program<Message> for Music {
geo_small_frame.push(small_frame.into_geometry());
}
let x = self.current_delta / self.length * w + 8.;
let x = self.current_delta / self.length * w + 8. - state.offset;
let mut frame_cursor = canvas::Frame::new(renderer, bounds.size());
frame_cursor.stroke_rectangle(
iced::Point::new(x, 0.),
@@ -251,8 +292,14 @@ impl canvas::Program<Message> for Music {
},
);
frame_cursor.stroke_rectangle(
iced::Point { x: 0.0, y: 0.0 },
frame.size(),
iced::Point {
x: -state.offset,
y: 0.0,
},
iced::Size {
width: global_width,
height: bounds.height,
},
Stroke {
width: 16.0,
style: Style::Solid(Color::from_rgb8(207, 74, 28)),
@@ -276,12 +323,21 @@ impl canvas::Program<Message> for Music {
cursor: Cursor,
) -> (Status, Option<Message>) {
//eprintln!("event = {:?}", event);
if let Event::Keyboard(keyboard_event) = &event {
match keyboard_event {
keyboard::Event::ModifiersChanged(m) => {
state.ctrl = m.control();
}
_ => (),
}
}
if let Event::Mouse(mouse_event) = event {
match mouse_event {
mouse::Event::ButtonPressed(mouse::Button::Left) => {
*state = true;
state.mouse_left = true;
if let Some(position) = cursor.position_in(bounds) {
let pos_x = (position.x - 8.0) / (bounds.width - 16.);
let pos_x =
(position.x + state.offset - 8.0) / (bounds.width * state.zoom - 16.);
let delta = (pos_x * self.length).clamp(0., self.length);
return (
Status::Captured,
@@ -289,19 +345,52 @@ impl canvas::Program<Message> for Music {
);
}
}
mouse::Event::ButtonReleased(mouse::Button::Left) => *state = false,
mouse::Event::ButtonReleased(mouse::Button::Left) => state.mouse_left = false,
mouse::Event::CursorMoved { position: _ } => {
if let Some(position) = cursor.position_in(bounds)
&& *state
&& state.mouse_left
{
let pos_x = (position.x - 8.0) / (bounds.width - 16.);
let pos_x =
(position.x + state.offset - 8.0) / (bounds.width * state.zoom - 16.);
let delta = (pos_x * self.length).clamp(0., self.length);
return (Status::Captured, Some(Message::ClickedOnTimeLine(delta)));
}
}
mouse::Event::WheelScrolled { delta: d } => {
if state.ctrl {
match d {
ScrollDelta::Lines { x: _, y } => {
let before = bounds.width * state.zoom;
state.zoom += y / 10.;
state.offset = state.offset * (bounds.width * state.zoom) / before;
}
ScrollDelta::Pixels { x: _, y } => {
let before = bounds.width * state.zoom;
state.zoom += y / 10.;
state.offset = state.offset * (bounds.width * state.zoom) / before;
}
}
} else {
match d {
ScrollDelta::Lines { x: _, y } => {
state.offset -= y * 32. * state.zoom;
}
ScrollDelta::Pixels { x: _, y } => {
state.offset -= y * 32. * state.zoom;
}
}
}
}
_ => {}
}
}
state.zoom = state.zoom.clamp(1.0, 10.);
if state.offset + bounds.width >= bounds.width * state.zoom {
state.offset = bounds.width * state.zoom - bounds.width
}
if state.offset <= 0. {
state.offset = 0.
}
(Status::Ignored, None)
}

View File

@@ -194,6 +194,7 @@ pub struct Polygon {
#[serde(skip)]
pub color: Color,
pub color_name: String,
#[serde(skip)]
pub show_color_picker: bool,
}
#[warn(dead_code)]